2 पॉइंट द्वारा GN⁺ 2024-11-30 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • tail -f /some/log/file | grep thing1 | grep thing2 जैसी पाइपलाइन, जहां धीरे-धीरे आने वाले output को कई commands से जोड़ा जाता है, असल में रुकी नहीं होती; बीच की command output को buffer में जमा कर रही होती है, इसलिए खाली दिख सकती है
  • grep और कई programs isatty से जांचते हैं कि stdout terminal है या नहीं; terminal होने पर line buffering, और pipe या file होने पर लगभग 8KB block buffering का उपयोग करते हैं
  • tail, cat, tee ऐसे उदाहरण हैं जो output buffering नहीं करते, लेकिन grep --line-buffered, sed -u, tcpdump -l, jq -u, tr -u जैसे buffering कम करने के विकल्प command के हिसाब से अलग होते हैं
  • Ctrl-C से pipeline तोड़ने पर tcpdump जैसे program के buffer में मौजूद output गायब हो सकता है; kill -TERM $PID से बंद करने पर buffer flush होकर output दिख सकता है
  • व्यावहारिक समाधान हैं: जल्दी खत्म होने वाली command में बदलना, grep --line-buffered, single awk या जटिल grep, stdbuf, unbuffer आदि, लेकिन हर एक के काम करने की शर्तें और side effects जांचने चाहिए

पाइप अटका हुआ क्यों दिखता है

  • जब log file में lines धीरे-धीरे जुड़ती हैं, तो नीचे दी गई pipeline में matching result होने पर भी output न दिख सकता है
    • tail -f /some/log/file | grep thing1 | grep thing2
  • वजह pipe खुद नहीं, बल्कि बीच में मौजूद grep thing1 है, जो result तुरंत लिखने के बजाय buffer में store करता है
  • अगर program हर बार तुरंत लिखे, तो system calls बढ़ जाते हैं; इसलिए performance के लिए वह कुछ data इकट्ठा करने के बाद pipe या file में लिखता है
  • इस उदाहरण में grep thing1 लगभग 8KB output जमा होने तक इंतजार कर सकता है, और धीमे log में यह condition practically कभी पूरी नहीं हो सकती

terminal और pipe में output का तरीका अलग होता है

  • tail -f file | grep thing ठीक काम करता है, लेकिन उसके बाद दूसरा grep जोड़ने पर output रुका हुआ लग सकता है
  • grep और कई programs isatty function से जांचते हैं कि stdout terminal है या नहीं
    • stdout terminal हो तो line buffering इस्तेमाल होती है और line-by-line तुरंत output आता है
    • stdout pipe या file हो तो block buffering इस्तेमाल होती है और एक निश्चित size से ज्यादा data जमा होने पर output आता है
  • इसलिए अगर grep सीधे terminal पर लिखता है तो line तुरंत दिखती है, लेकिन अगली command से जुड़े pipe में लिखने पर दिख नहीं सकती
  • buffer size program के हिसाब से अलग होता है
    • grep में libc buffering संभालता है, और libc का size BUFSIZ variable से define होता है
    • glibc में इसकी definition stdio.h में है
  • terminal पर लिखते समय 8KB output buffer न इस्तेमाल करना कोई भौतिक नियम नहीं है; program चाहे तो ऐसा implement कर सकता है, लेकिन वह बेहद अजीब behavior के करीब होगा

हर command का buffering behavior अलग होता है

  • output buffering मुश्किल इसलिए है क्योंकि user को याद रखना पड़ता है कि कौन-सी command pipe output में buffering करती है
  • output buffering न करने वाली commands के उदाहरण ये हैं
    • tail
    • cat
    • tee
  • pipe में लिखते समय output buffering करने वाली आम commands और उसे कम करने के तरीके ये हैं
    • grep: --line-buffered
    • sed: -u
    • awk: fflush() function
    • tcpdump: -l
    • jq: -u
    • tr: -u
    • cut: buffering disable नहीं की जा सकती
  • sort जैसी commands, जिन्हें काम करने के लिए पूरा input मिलने के बाद ही आगे बढ़ना होता है, उनमें buffering practically मायने नहीं रखती
  • Mac OS और GNU versions दोनों test करने की कोशिश की गई, लेकिन variants बहुत हैं, इसलिए कुछ गलतियां हो सकती हैं

programming languages का default output भी buffer होता है

  • कुछ programming languages का default print output भी pipe में लिखते समय buffer होता है
  • language के हिसाब से disable करने के तरीके ये हैं
    • C: setvbuf
    • Python: python -u, PYTHONUNBUFFERED=1, sys.stdout.reconfigure(line_buffering=False), print(x, flush=True)
    • Ruby: STDOUT.sync = true
    • Perl: $| = 1
  • यह default behavior batch processing में default output functions को तेज बनाने के लिए design किया गया लगता है
  • output के तरीके के हिसाब से buffering बदल सकती है
    • C++ में cout << "hello\n" pipe में लिखते समय buffer होता है
    • cout << "hello" << endl output को flush करता है

Ctrl-C और file redirection में फर्क

  • अगर नीचे की तरह tcpdump output को grep से जोड़ते समय -l छूट जाए, तो output buffer में रह सकता है
    • sudo tcpdump -ni any port 53 | grep example.com
  • आदर्श रूप से उम्मीद की जा सकती है कि Ctrl-C दबाने पर tcpdump buffer flush करेगा और grep search करके छूटा हुआ output दिखा देगा
  • असल में programs बंद होते समय tcpdump buffer में मौजूद output lost हो जाता है
  • strace से जांचने पर दिखा कि grep को tcpdump से पहले SIGINT मिलता है, इसलिए अगर tcpdump flush करने की कोशिश भी करे, तो grep पहले ही खत्म हो चुका हो सकता है
  • workaround के तौर पर tcpdump का PID ढूंढकर kill -TERM $PID चलाने से tcpdump buffer flush कर सकता है और output दिख सकता है
  • file redirection भी buffering करता है
    • sudo tcpdump -ni any port 53 > output.txt
  • हालांकि file redirection में, Ctrl-C से buffer content पूरी तरह उड़ जाने वाली समस्या के विपरीत, experience में program बंद होने से पहले buffer content अक्सर file में लिख जाता है
  • यह behavior हमेशा भरोसेमंद है या नहीं, यह साफ नहीं है

buffering से बचने के पांच तरीके

  • जल्दी खत्म होने वाले program में बदलना

    • धीरे-धीरे pipe में लिखने वाली स्थिति से बचकर, जल्दी खत्म होने वाली command में बदला जा सकता है
    • उदाहरण इस तरह है
      • cat /some/log/file | grep thing1 | grep thing2 | tail
    • यह original tail -f command जैसा behavior नहीं है, लेकिन जटिल buffering problem से बच सकता है
  • grep का line buffer option इस्तेमाल करना

    • grep में buffering से बचने के लिए flag है
    • उदाहरण इस तरह है
      • tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
  • awk या ज्यादा जटिल grep में मिलाना

    • कई grep इस्तेमाल करने वाली स्थिति को single awk में बदला जा सकता है
      • tail -f /some/log/file | awk '/thing1/ && /thing2/'
    • या ज्यादा जटिल regular expression वाले grep से लिखा जा सकता है
      • tail -f /some/log/file | grep -E 'thing1.*thing2'
    • awk भी buffering करता है, इसलिए यह तरीका काम करे, इसके लिए awk pipeline की last command होना चाहिए
  • stdbuf इस्तेमाल करना

    • stdbuf LD_PRELOAD का इस्तेमाल करके libc की buffering बंद करता है
    • output buffering बंद करने का उदाहरण यह है
      • tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
    • LD_PRELOAD based solutions की तरह reliability सीमित है
      • static binary में काम नहीं करता
      • अगर program libc buffering इस्तेमाल नहीं करता, तो काम न कर सकता है
      • Mac OS पर हमेशा काम नहीं करता
    • संबंधित explanation के लिए Harry Marr का How stdbuf works है
  • unbuffer इस्तेमाल करना

    • unbuffer program program output को TTY जैसा होने के लिए force करता है, ताकि वह सामान्य TTY की तरह कम buffering करे और color output आदि इस्तेमाल करे
    • उदाहरण इस तरह है
      • tail -f /some/log/file | unbuffer grep thing1 | grep thing2
    • stdbuf के विपरीत यह हमेशा काम करता है, लेकिन unwanted side effects हो सकते हैं
      • जैसे grep thing1 matching results में color जोड़ सकता है
    • unbuffer expect package में आता है

जहां समस्या आम तौर पर दिखती है और environment variable idea

  • यह समस्या मुख्य रूप से उन programs में दिखती है जो pipe में data धीरे-धीरे भेजते हैं
  • उदाहरण ये हैं
    • tcpdump
    • tail -f
    • kubectl logs जैसे log watching तरीके
    • धीमे calculation का output
  • Python के PYTHONUNBUFFERED की तरह buffering बंद करने के लिए कोई standard environment variable हो तो अच्छा हो सकता है
  • यह idea Mark Dominus के 2018 के blog post और follow-up post से मिला
  • नाम के उदाहरण के तौर पर NO_COLOR की तरह NO_BUFFER हो सकता है
  • design मुश्किल है
    • NETBSD में ज्यादा control देने वाले STDBUF, STDBUF1 जैसे environment variables हैं
    • ज्यादातर developers शायद relatively छोटे edge case के लिए कई environment variables implement नहीं करना चाहेंगे
  • यह भी जिज्ञासा है कि क्या ऐसे programs हैं जो output buffer को 1 second जैसे interval पर automatically flush करते हैं, लेकिन ऐसा कोई program याद नहीं आता और इसके नुकसान हो सकते हैं

जिन बातों को शामिल नहीं किया गया

  • line buffering और completely unbuffered output का फर्क शामिल नहीं है
  • stderr buffering और stdout buffering का फर्क शामिल नहीं है
  • यह सामग्री सिर्फ program के अंदर होने वाली buffering पर चर्चा करती है
  • operating system का TTY driver भी कभी-कभी थोड़ी buffering करता है
  • pipe में लिखने की स्थिति के अलावा output flush करने के दूसरे कारण शामिल नहीं हैं

1 टिप्पणियां

 
GN⁺ 2024-11-30
Hacker News की राय
  • buffered access लगभग हमेशा byte threshold तक पहुँचने पर, या कम से कम 1 byte मौजूद होने पर कुछ समय बाद flush करने वाले “threshold या timeout” तरीके पर होना चाहिए
    इसी तरह की समस्या सुलझाने के लिए hardware interface में यह एक आम तरीका है
    इस स्थिति में user space में buffering करने वाली library को जब data पहली बार buffer में डाला जाए, तभी उपयुक्त timer सेट करना चाहिए। timeout value को argument के रूप में लिया जा सकता है, या इंसान को छोटा महसूस होने वाला लगभग 1~100ms रखा जा सकता है, या {bandwidth / threshold} के अनुपात में रखा जा सकता है, या इस तरह तय किया जा सकता है कि system call overhead कुल समय के 0.1% से अधिक न हो
    यह तरीका सिर्फ write पर नहीं, read पर भी लागू होता है। अगर batch read या merged read किया जा रहा है, तो ऐसा ही तरीका चाहिए, लेकिन इसके लिए data channel में “pending data” को efficiently query करने या notification पाने का तरीका होना चाहिए, इसलिए यह channel design पर ज़्यादा निर्भर करता है। hardware में interrupt coalescing जैसी तकनीक आम है

    • मुझे लगता है दिशा सही है, लेकिन अगर libc automatic timer सेट करे तो expected behavior बदल जाएगा और कई पेचीदा समस्याएँ पैदा होंगी
      I/O error सिर्फ write के समय ही नहीं बल्कि किसी भी समय आ सकता है, और timer की वजह से कई system call रुक सकते हैं, न कि सिर्फ वहाँ जहाँ program ने खुद timer लगाया हो या जहाँ signal पहुँचा हो
      अगर application और libc दोनों timer सेट करें तो भ्रम पैदा हो सकता है। आजकल kernel timer API शायद पहले की तुलना में बेहतर है, इसलिए यह बात कम प्रासंगिक हो सकती है, लेकिन अगर application किसी महत्वपूर्ण हिस्से में थोड़ी देर के लिए signal block कर दे तो I/O timer भी प्रभावित होगा
      signal handling के समय और तरीके की वजह से I/O structure access को और सावधानी से करना होगा
    • ऐसे timeout को transparent तरीके से संभालना POSIX और ISO C की सीमाओं के तहत मुश्किल लगता है। application layer के कुछ सहयोग की ज़रूरत दिखती है
    • सामान्य Linux alarm signal-based होते हैं, इसलिए उन्हें manage करना बहुत मुश्किल है, और reschedule करने के लिए kernel में जाना पड़ता है, जिससे performance पर असर पड़ सकता है
      io_uring और user space timer इस्तेमाल करें तो यह कहीं बेहतर scale करता है, लेकिन तेज़ छोटे writes को बड़ी संख्या में support करने के लिए फिर भी कुछ tricks चाहिए। उदाहरण के लिए, लगभग 10 लाख बार प्रति सेकंड से ऊपर जाते ही timer management की लागत दिखने लगती है, और 10 करोड़ writes प्रति सेकंड तक पहुँचने के लिए काफ़ी अजीब तकनीकों की ज़रूरत पड़ी थी
    • सहमत होना मुश्किल है। यहाँ buffering वही कर रही है जो उसे मूल रूप से करना चाहिए
      समस्या की जड़ यह है कि interactive होना चाहिए वाली चीज़ और ऐसे contract का मिश्रण हो गया है जिसमें interactivity मानी ही नहीं गई। उदाहरण के लिए tail के follow output को pipe में भेजना
      मुझे नहीं लगता कि यहाँ कोई वास्तविक समस्या है जिसे हल करना चाहिए। hardware analogy लें तो यह बारिश का पानी जमा करने वाली टंकी जैसा है, जिसे सिर्फ भर जाने पर ही आगे भेजा जाता है। पता नहीं कौन-सा उदाहरण ध्यान में रखा गया है, लेकिन मेरी जानकारी में hardware में time-based flush आम नहीं है
      प्रस्तावित बदलाव contract को बहुत ज़्यादा जटिल बना देगा
    • मेरे हिसाब से predictable footgun बेहतर है। idea अच्छा है, लेकिन इसे अलग flag होना चाहिए, और तब उसके अस्तित्व के बारे में पता होना भी ज़रूरी होगा
      समस्या semantics से ज़्यादा semantics के बारे में न जानने में है
  • मैंने NIX systems के साथ 20 साल से अधिक काम किया है, और मुझे पता है कि ऐसा होता है, फिर भी हर बार आउटपुट क्यों नहीं आ रहा यह समझते-समझते काफी देर puzzling करने के बाद ही याद आता है

  • “हाल की पोस्टें काफ़ी लंबी होती जा रही हैं, क्या buffering पर 3000 शब्दों का लेख सच में कोई पढ़ना चाहेगा” — इस हिस्से पर, व्यक्तिगत रूप से मैं तो पढ़ना चाहूँगा

    • यह लेख पर निर्भर करता है
      कभी-कभी लंबा लेख search optimization के लिए अनावश्यक भराव भी हो सकता है
    • ऐसे मामलों में AI summary काफ़ी मददगार हो सकती है। लेख का summary बनवाकर फिर उसे review किया जा सकता है
      TLDR और NTLDR, यानी “लंबा था लेकिन पढ़ लिया” सेक्शन भी रखे जा सकते हैं
  • अच्छा होगा अगर पूरे system का CPU idle होते ही सभी buffers flush हो जाएँ
    buffering मूलतः CPU बचाने की तकनीक है। अगर CPU असीमित हो, तो हर buffer 1 byte का होगा। buffer efficiency के लिए data इकट्ठा करके batch में process करने का तरीका है
    लेकिन जब CPU idle हो जाए, तब “बाद में करने वाला काम” बचा नहीं होना चाहिए। जैसे ही kernel scheduler idle state में जाए, उसे सभी process को buffer flush करो वाला signal भेजना चाहिए

    • दिलचस्प विचार है। लेकिन सभी process को signal भेजना बहुत महँगा लगता है
      वह सारा काम करके फिर buffer flush के लिए system call भी करवाना पड़ेगा। शायद ऐसा mechanism जोड़ा जा सकता है जिसमें kernel को user space buffer का पता हो और idle होने पर वह वहीं से सीधे उठा ले
      क्या यह कुछ हद तक io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff... जैसा है?
    • शानदार विचार है, लेकिन सब कुछ एक साथ करना शायद बेहतर नहीं होगा। और low-power systems में यह अनुमान के आधार पर सोए हुए process को जगा कर efficiency घटा सकता है
  • यह लेख unbuffered और line buffering — इन दो अलग चीज़ों को गड़बड़ा रहा है
    unbuffered mode बेवजह performance खराब करता है, और अगर कई source एक ही pipe में लिख रहे हों तो गलत output पैदा कर सकता है। बहुत लंबी lines तो वैसे भी मिल जाएँगी, लेकिन वास्तविक दुनिया में ज़्यादातर output lines formatting/control characters और supplementary plane characters के साथ भी 4096 bytes से छोटी होती हैं
    line buffering terminal का default है, और pipe में भी अक्सर यही desired behavior होता है। हर command को stdbuf -oL -eL के तहत चलाने से यह हो जाता है। जो दुर्लभ programs line के बीच update चाहते हैं, उन्हें वैसे भी manual flush करना पड़ता है, इसलिए यहाँ भी वे सही चलेंगे
    stdbuf असल में यह करता है, ऐसा समझा जा सकता है:
    env -i \command -v stdbuf` -oL -eL `command -v env``

  • मैंने पहले इस समस्या पर एक लेख लिखा था: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
    non-buffering command के बारे में यह implementation-dependent है, या cat के मामले में गलत भी हो सकता है। https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... और -u देखें। POSIX ने इसे नियंत्रित करने का कोई आधिकारिक तरीका नहीं दिया, यह काफ़ी दर्दनाक है
    एक और चीज़ जिसका ज़िक्र नहीं हुआ, वह है input buffering, और यह ऐसा अजीब नतीजा देती है:
    $ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }
    v1=1
    v2=
    इस मामले में समाधान stdbuf -i0 head -1 का उपयोग करना है

    • मुझे नहीं लगता कि pipe या socketpair जैसी जगह से पढ़ने वाला process, लिखने वाले process पर ऐसी पाबंदियाँ लागू कर सकता है। ptrace() जैसे भारी-भरकम hack के मामले को छोड़कर
      pipe buffer size को adjust करना शायद संभव हो, लेकिन standard C I/O को उसका पालन करना चाहिए, ऐसी कोई परंपरा मुझे नहीं पता
      वैसे भी इस मामले में stdbuf मददगार नहीं लगता:
      $ ./a | stdbuf -i0 -- cat
      #include
      #include
      int main(void) {
      for (;;) {
      printf("n");
      usleep(100000);
      }
      }
  • buffering होने के अच्छे कारण हैं। स्क्रीन पर output दिखाना, buffer में लिखने की तुलना में काफ़ी ज़्यादा धीमा होता है
    characters को एक-एक करके output करना बेहद अक्षम है
    यह बहुत पुरानी समस्या है, और UART के साथ काम करते समय अक्सर सामने आती है। संभावित समाधान कई हैं: line-based तरीका, जिसमें newline जैसे special character से output के अंत को चिह्नित किया जाता है; length-based तरीका, जिसमें 8KB जैसी लंबाई पूरी होने तक इंतज़ार किया जाता है; और time-based तरीका, जिसमें हर X millisecond पर output किया जाता है
    हर तरीके के अपने फ़ायदे और नुकसान हैं, और कौन-सा सबसे अच्छा है यह application पर निर्भर करता है। लेख में जहाँ कहा गया है कि कुछ programs buffering का उपयोग नहीं करते, मुझे वह गलत लगता है। वे programs बस साफ़ तौर पर length-based तरीका नहीं अपनाते

    • जब interface से एक-दो layer ऊपर constraints पता हों, तब यह सबसे अच्छा काम करता है
      line-based approach ऐसा ही एक तरीका है, लेकिन इसमें इस बात पर सहमति चाहिए कि कौन-सा character इस्तेमाल होगा। आमतौर पर वह newline होता है
    • यह सिर्फ़ backend में actual write को संभालने की लागत का मामला नहीं है। /dev/null पर इतनी अधिक system calls करना भर भी performance को काफ़ी गिरा सकता है
  • मैं 35 साल से ज़्यादा समय से Unix इस्तेमाल कर रहा हूँ, लेकिन यह कैसे काम करता है इसे मैंने कभी पूरी तरह नहीं समझा
    buffering के व्यवहार को कई systems और components में समग्र रूप से समझाने वाला यह विवरण अच्छा लगा, और मैंने इसमें से निश्चित रूप से कुछ सीखा

  • “pipe में Ctrl-C दबाने पर buffer की सामग्री गायब हो जाती है” वाले हिस्से पर, मेरा ख़याल है कि ज़्यादातर programs SIGINT पर buffer को flush करेंगे
    लेकिन shell में ऐसा होने के लिए SIGINT केवल pipeline के पहले program को देना होगा, और शायद वास्तविक व्यवहार ऐसा नहीं है

    • जहाँ तक मुझे याद है, आख़िरी process को sigint मिलता है और बाकी को sigpipe