2 คะแนน โดย GN⁺ 2024-11-30 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • pipeline ที่เชื่อมเอาต์พุตที่ไหลเข้ามาช้า ๆ ผ่านหลายคำสั่ง เช่น tail -f /some/log/file | grep thing1 | grep thing2 อาจดูเหมือนว่างเปล่า ไม่ใช่เพราะค้างจริง แต่เพราะคำสั่งตรงกลาง เก็บเอาต์พุตไว้ในบัฟเฟอร์
  • grep และหลายโปรแกรมตรวจสอบด้วย isatty ว่า stdout เป็นเทอร์มินัลหรือไม่ ถ้าเป็นเทอร์มินัลจะใช้ line buffering แต่ถ้าเป็น pipe หรือไฟล์จะใช้ block buffering ประมาณหน่วยละ 8KB
  • tail, cat, tee เป็นตัวอย่างที่ไม่ทำ output buffering แต่ตัวเลือกเพื่อลด buffering แตกต่างกันไปตามคำสั่ง เช่น grep --line-buffered, sed -u, tcpdump -l, jq -u, tr -u
  • หากตัด pipeline ด้วย Ctrl-C เอาต์พุตที่ค้างอยู่ในบัฟเฟอร์ของโปรแกรมอย่าง tcpdump อาจหายไปได้ แต่ถ้าจบโปรแกรมด้วย kill -TERM $PID บัฟเฟอร์อาจถูก flush จนเห็นเอาต์พุต
  • วิธีแก้ในทางปฏิบัติคือเปลี่ยนไปใช้คำสั่งที่จบเร็ว, grep --line-buffered, awk ตัวเดียวหรือ grep ที่ซับซ้อนขึ้น, stdbuf, unbuffer เป็นต้น แต่แต่ละวิธีต้องตรวจสอบ เงื่อนไขการทำงานและผลข้างเคียง

ทำไม pipe จึงดูเหมือนค้าง

  • เมื่อมีบรรทัดค่อย ๆ ถูกเพิ่มเข้าไปในไฟล์ log pipeline ต่อไปนี้อาจไม่แสดงเอาต์พุต แม้จะมีผลลัพธ์ที่ match ก็ตาม
    • tail -f /some/log/file | grep thing1 | grep thing2
  • สาเหตุไม่ใช่ตัว pipe เอง แต่เป็นเพราะ grep thing1 ที่อยู่ตรงกลางไม่ได้เขียนผลลัพธ์ทันที แต่ เก็บไว้ในบัฟเฟอร์
  • ถ้าโปรแกรมเขียนออกทุกครั้งทันที จำนวน system call จะเพิ่มขึ้น จึงมักรวบรวมข้อมูลระดับหนึ่งเพื่อประสิทธิภาพก่อนเขียนไปยัง pipe หรือไฟล์
  • ในตัวอย่างนี้ grep thing1 อาจรอจนมีเอาต์พุตสะสมประมาณ 8KB และใน log ที่ไหลช้า เงื่อนไขนั้นอาจแทบไม่มีวันเกิดขึ้น

วิธีเอาต์พุตที่ต่างกันระหว่างเทอร์มินัลกับ pipe

  • tail -f file | grep thing ทำงานได้ดี แต่ถ้าต่อ grep ตัวที่สองด้านหลัง อาจดูเหมือนเอาต์พุตค้าง
  • grep และหลายโปรแกรมใช้ฟังก์ชัน isatty เพื่อตรวจว่า stdout เป็นเทอร์มินัลหรือไม่
    • ถ้า stdout เป็น เทอร์มินัล จะใช้ line buffering และแสดงผลทันทีเป็นรายบรรทัด
    • ถ้า stdout เป็น pipe หรือไฟล์ จะใช้ block buffering และจะแสดงผลเมื่อข้อมูลสะสมถึงขนาดหนึ่ง
  • ดังนั้นถ้า grep เขียนตรงไปที่เทอร์มินัล จะเห็นบรรทัดทันที แต่ถ้าเขียนไปยัง pipe ที่ส่งต่อให้คำสั่งถัดไป อาจไม่เห็น
  • ขนาดบัฟเฟอร์แตกต่างกันไปตามโปรแกรม
    • grep ให้ libc จัดการ buffering และขนาดของ libc ถูกกำหนดด้วยตัวแปร BUFSIZ
    • ตำแหน่งนิยามของ glibc อยู่ใน stdio.h
  • การไม่ใช้บัฟเฟอร์เอาต์พุต 8KB เมื่อเขียนไปยังเทอร์มินัลไม่ใช่กฎฟิสิกส์ หากโปรแกรมต้องการก็ทำเช่นนั้นได้ แต่ถือว่าเป็นพฤติกรรมที่แปลกมาก

พฤติกรรม buffering ที่ต่างกันในแต่ละคำสั่ง

  • เหตุผลที่ output buffering รับมือยากคือผู้ใช้ต้องจำว่าคำสั่งใดทำ buffering เมื่อส่งเอาต์พุตไปยัง pipe
  • ตัวอย่างคำสั่งที่ไม่ทำ output buffering มีดังนี้
    • tail
    • cat
    • tee
  • คำสั่งที่พบบ่อยซึ่งทำ output buffering เมื่อเขียนไปยัง pipe และวิธีลดผลกระทบ มีดังนี้
    • grep: --line-buffered
    • sed: -u
    • awk: ฟังก์ชัน fflush()
    • tcpdump: -l
    • jq: -u
    • tr: -u
    • cut: ปิด buffering ไม่ได้
  • สำหรับคำสั่งอย่าง sort ที่ต้องรับอินพุตทั้งหมดก่อนจึงทำงานได้ ประเด็นเรื่อง buffering แทบไม่มีความสำคัญในทางปฏิบัติ
  • พยายามทดสอบทั้ง Mac OS และเวอร์ชัน GNU แล้ว แต่มี variant จำนวนมาก จึงอาจมีข้อผิดพลาดบางส่วนได้

เอาต์พุตเริ่มต้นของภาษาโปรแกรมก็ทำ buffering

  • เอาต์พุต print เริ่มต้นของภาษาโปรแกรมบางภาษา ก็ทำ buffering เมื่อเขียนไปยัง pipe
  • วิธีปิดตามแต่ละภาษา มีดังนี้
    • C: setvbuf
    • Python: python -u, PYTHONUNBUFFERED=1, sys.stdout.reconfigure(line_buffering=False), print(x, flush=True)
    • Ruby: STDOUT.sync = true
    • Perl: $| = 1
  • พฤติกรรมเริ่มต้นแบบนี้ดูเหมือนเป็นการออกแบบเพื่อให้ฟังก์ชันเอาต์พุตพื้นฐานเร็วขึ้นในการประมวลผลแบบ batch
  • buffering อาจต่างกันตามวิธีเขียนเอาต์พุต
    • ใน C++ cout << "hello\n" จะทำ buffering เมื่อเขียนไปยัง pipe
    • cout << "hello" << endl จะ flush เอาต์พุต

ความแตกต่างที่เกิดกับ Ctrl-C และ file redirection

  • หากเชื่อมเอาต์พุตของ tcpdump ไปยัง grep แบบต่อไปนี้และลืม -l เอาต์พุตอาจค้างอยู่ในบัฟเฟอร์
    • sudo tcpdump -ni any port 53 | grep example.com
  • ตามอุดมคติ เมื่อกด Ctrl-C อาจคาดหวังว่า tcpdump จะ flush บัฟเฟอร์ แล้ว grep จะค้นหาและแสดงเอาต์พุตที่ตกหล่น
  • แต่ในความเป็นจริง เมื่อโปรแกรมต่าง ๆ จบการทำงาน เอาต์พุตที่อยู่ในบัฟเฟอร์ของ tcpdump จะ สูญหาย
  • เมื่อตรวจด้วย strace พบว่า grep ได้รับ SIGINT ก่อน tcpdump ดังนั้นแม้ tcpdump จะพยายาม flush แต่ grep อาจตายไปแล้ว
  • วิธีเลี่ยงคือหา PID ของ tcpdump แล้วรัน kill -TERM $PID ซึ่งอาจทำให้ tcpdump flush บัฟเฟอร์จนเห็นเอาต์พุตได้
  • file redirection ก็ทำ buffering เช่นกัน
    • sudo tcpdump -ni any port 53 > output.txt
  • อย่างไรก็ตาม file redirection ต่างจากปัญหาที่ Ctrl-C ทำให้เนื้อหาในบัฟเฟอร์หายไปทั้งหมด โดยจากประสบการณ์มักพบว่าเนื้อหาในบัฟเฟอร์ถูกเขียนลงไฟล์ก่อนโปรแกรมจบ
  • ยังไม่แน่ชัดว่าสามารถเชื่อถือพฤติกรรมนี้ได้เสมอหรือไม่

ห้าวิธีในการหลีกเลี่ยง buffering

  • เปลี่ยนไปใช้โปรแกรมที่จบเร็ว

    • หลีกเลี่ยงสถานการณ์ที่เขียนไปยัง pipe อย่างช้า ๆ แล้วเปลี่ยนเป็นคำสั่งที่จบเร็วได้
    • ตัวอย่างมีดังนี้
      • cat /some/log/file | grep thing1 | grep thing2 | tail
    • แม้พฤติกรรมจะไม่เหมือนคำสั่ง tail -f เดิม แต่ช่วยเลี่ยงปัญหา buffering ที่ซับซ้อนได้
  • ใช้ตัวเลือก line buffer ของ grep

    • grep มี flag สำหรับหลีกเลี่ยง buffering
    • ตัวอย่างมีดังนี้
      • tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
  • รวมด้วย awk หรือ grep ที่ซับซ้อนขึ้น

    • กรณีที่ใช้ grep หลายตัว สามารถเปลี่ยนเป็น awk ตัวเดียวได้
      • tail -f /some/log/file | awk '/thing1/ && /thing2/'
    • หรือใช้ grep ที่มี regular expression ซับซ้อนขึ้นได้
      • tail -f /some/log/file | grep -E 'thing1.*thing2'
    • awk ก็ทำ buffering เช่นกัน ดังนั้นวิธีนี้จะทำงานได้เมื่อ awk เป็น คำสั่งสุดท้าย ของ pipeline
  • ใช้ stdbuf

    • stdbuf ใช้ LD_PRELOAD เพื่อปิด buffering ของ libc
    • ตัวอย่างการปิด output buffering มีดังนี้
      • tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
    • วิธีที่อาศัย LD_PRELOAD มีความน่าเชื่อถือจำกัด
      • ไม่ทำงานกับ binary แบบ static
      • อาจไม่ทำงานหากโปรแกรมไม่ได้ใช้ buffering ของ libc
      • ไม่ได้ทำงานเสมอไปบน Mac OS
    • มีคำอธิบายที่เกี่ยวข้องคือ How stdbuf works ของ Harry Marr
  • ใช้ unbuffer

    • unbuffer program บังคับให้เอาต์พุตของโปรแกรมเหมือนเป็น TTY ทำให้ buffering น้อยลงเหมือนใน TTY ปกติ และใช้สิ่งอย่าง output สี
    • ตัวอย่างมีดังนี้
      • tail -f /some/log/file | unbuffer grep thing1 | grep thing2
    • ต่างจาก stdbuf ตรงที่มักทำงานได้เสมอ แต่อาจมีผลข้างเคียงที่ไม่ต้องการ
      • เช่น grep thing1 อาจใส่สีให้ผลลัพธ์ที่ match
    • unbuffer อยู่ในแพ็กเกจ expect

สถานการณ์ที่มักเห็นปัญหาและไอเดียเรื่อง environment variable

  • ปัญหาแบบนี้มักเกิดกับโปรแกรมที่ค่อย ๆ ปล่อยข้อมูลไปยัง pipe
  • ตัวอย่างมีดังนี้
    • tcpdump
    • tail -f
    • การเฝ้าดู log แบบ kubectl logs
    • เอาต์พุตจากการคำนวณที่ช้า
  • คงจะดีหากมี environment variable มาตรฐานสำหรับปิด buffering เหมือน PYTHONUNBUFFERED ของ Python
  • ไอเดียนี้มาจาก บทความบล็อก ปี 2018 ของ Mark Dominus และ บทความต่อเนื่อง
  • ตัวอย่างชื่ออาจเป็น NO_BUFFER ในลักษณะเดียวกับ NO_COLOR
  • การออกแบบทำได้ยาก
    • NETBSD มี environment variable เช่น STDBUF, STDBUF1 และอื่น ๆ ที่ให้การควบคุมจำนวนมาก
    • นักพัฒนาส่วนใหญ่อาจไม่อยาก implement environment variable หลายตัวเพื่อ edge case ที่ค่อนข้างเล็ก
  • ยังสงสัยด้วยว่ามีโปรแกรมที่ flush บัฟเฟอร์เอาต์พุตอัตโนมัติตามรอบเวลา เช่น ทุก 1 วินาทีหรือไม่ แต่นึกโปรแกรมแบบนั้นไม่ออก และอาจมีข้อเสีย

ขอบเขตที่ไม่ได้กล่าวถึง

  • ไม่ครอบคลุมความแตกต่างระหว่าง line buffering กับเอาต์พุตแบบไม่ใช้บัฟเฟอร์เลย
  • ไม่ครอบคลุมความแตกต่างระหว่าง buffering ของ stderr กับ stdout
  • เนื้อหานี้กล่าวถึงเฉพาะ buffering ที่เกิดภายในโปรแกรม
  • TTY driver ของระบบปฏิบัติการก็ทำ buffering เล็กน้อยเป็นครั้งคราวเช่นกัน
  • ไม่ครอบคลุมเหตุผลอื่น ๆ ที่ต้อง flush เอาต์พุตนอกเหนือจากสถานการณ์ที่เขียนไปยัง pipe

1 ความคิดเห็น

 
GN⁺ 2024-11-30
ความคิดเห็นจาก Hacker News
  • วิธีเข้าถึงแบบมีบัฟเฟอร์แทบจะควรเป็นแบบ “ถึงค่าเกณฑ์หรือหมดเวลา” เสมอ กล่าวคือฟลัชเมื่อถึง ค่าเกณฑ์ของจำนวนไบต์ หรือเมื่อเวลาผ่านไประยะหนึ่งหากมีข้อมูลอยู่อย่างน้อย 1 ไบต์
    เป็นวิธีที่พบได้ทั่วไปในอินเทอร์เฟซฮาร์ดแวร์เพื่อแก้ปัญหาคล้าย ๆ กัน
    ในกรณีนี้ ไลบรารีที่ทำบัฟเฟอร์ในพื้นที่ผู้ใช้ควรตั้งตัวจับเวลาที่เหมาะสมเมื่อใส่ข้อมูลลงบัฟเฟอร์ครั้งแรก ค่าหมดเวลาอาจรับเป็นอาร์กิวเมนต์ ตั้งไว้สั้นพอที่มนุษย์จะรู้สึกว่าสั้น เช่น 1–100ms ตั้งให้แปรผันตาม {แบนด์วิดท์ / ค่าเกณฑ์} หรือกำหนดไม่ให้โอเวอร์เฮดของ system call เกิน 0.1% ของเวลารวมก็ได้
    วิธีนี้ใช้ได้กับการอ่านเช่นเดียวกับการเขียน หากทำการอ่านแบบรวมเป็นชุดหรืออ่านแบบรวมหลายรายการ ก็ต้องใช้แนวทางคล้ายกัน แต่ช่องทางข้อมูลต้องมีวิธีสอบถามหรือรับแจ้ง “ข้อมูลที่รออยู่” ได้อย่างมีประสิทธิภาพ จึงขึ้นกับการออกแบบช่องทางมากกว่า ในฮาร์ดแวร์ วิธีอย่างการรวมอินเทอร์รัปต์เป็นเรื่องพบได้ทั่วไป

    • ผมคิดว่าทิศทางนี้ถูกต้อง แต่ถ้า libc ตั้ง ตัวจับเวลาอัตโนมัติ พฤติกรรมที่คาดไว้จะเปลี่ยนไปและเกิดปัญหายุ่งยากมากมาย
      ข้อผิดพลาด I/O อาจเกิดขึ้นได้ทุกเมื่อ ไม่ใช่แค่ตอนเขียน และ system call หลายตัวอาจถูกขัดจังหวะโดยตัวจับเวลาได้ ไม่ใช่แค่จุดที่โปรแกรมตั้งตัวจับเวลาเองหรือจุดที่สัญญาณมาถึง
      หากทั้งแอปพลิเคชันและ libc ตั้งตัวจับเวลา ก็มีโอกาสทำให้สับสนได้เช่นกัน แม้ API ตัวจับเวลาของเคอร์เนลยุคนี้ดูดีกว่าที่จำได้ในอดีต จึงอาจเกี่ยวข้องน้อยลง แต่ถ้าแอปพลิเคชันบล็อกสัญญาณชั่วคราวในช่วงสำคัญ ก็จะกระทบตัวจับเวลา I/O ด้วย
      เนื่องจากจังหวะและวิธีจัดการสัญญาณ จึงต้องระวัง การเข้าถึงโครงสร้างข้อมูล I/O มากขึ้น
    • การจัดการ timeout แบบโปร่งใสเช่นนั้นน่าจะยุ่งยากภายใต้ข้อจำกัดของ POSIX และ ISO C ดูเหมือนต้องอาศัยความร่วมมือจากชั้นแอปพลิเคชันในระดับหนึ่ง
    • alarm ทั่วไปบน Linux อิงกับสัญญาณ จึงจัดการยากมาก และถ้าจะจัดตารางใหม่ก็ต้องเข้าเคอร์เนล ซึ่งอาจมีผลต่อประสิทธิภาพ
      ใช้ io_uring กับตัวจับเวลาในพื้นที่ผู้ใช้จะขยายสเกลได้ดีกว่ามาก แต่ถ้าต้องรองรับการเขียนขนาดเล็กที่รวดเร็วจำนวนมาก ก็ยังต้องใช้ลูกเล่นอยู่ดี ตัวอย่างเช่น เมื่อเกินราว 1 ล้านครั้งต่อวินาที ต้นทุนการจัดการตัวจับเวลาจะเริ่มเห็นชัดขึ้น และถ้าจะไปถึงการเขียน 100 ล้านครั้งต่อวินาที ก็ต้องใช้เทคนิคค่อนข้างพิสดาร
    • เห็นด้วยยากนะ บัฟเฟอร์ตรงนี้กำลังทำสิ่งที่มันควรทำอยู่แล้ว
      สาเหตุของปัญหาคือมีการปะปนกันระหว่างสิ่งที่ควรเป็นแบบโต้ตอบกับสัญญาที่ไม่ได้ตั้งอยู่บนสมมติฐานเรื่องการโต้ตอบ เช่น กรณีส่งเอาต์พุตติดตามของ tail เข้าไปใน pipe
      ผมไม่คิดว่ามีปัญหาจริงที่ต้องแก้ เปรียบกับฮาร์ดแวร์ก็เหมือนแท็งก์เก็บน้ำฝน ที่จะย้ายต่อเมื่อเต็มแล้วเท่านั้น ไม่รู้ว่าคุณนึกถึงตัวอย่างไหน แต่เท่าที่ผมรู้ การฟลัชตามเวลา ไม่ได้พบบ่อยในฮาร์ดแวร์
      การแก้ไขที่เสนอทำให้สัญญาซับซ้อนขึ้นมาก
    • ผมคิดว่า footgun ที่คาดเดาได้ยังดีกว่า ไอเดียดี แต่ควรเป็นแฟล็กแยกต่างหาก และถ้าเป็นอย่างนั้นก็ต้องรู้ว่ามันมีอยู่
      ปัญหาไม่ได้อยู่ที่ semantics เองเท่าไร แต่อยู่ที่ ไม่รู้ semantics ต่างหาก
  • ใช้ระบบ NIX มามากกว่า 20 ปี และรู้ว่าเรื่องแบบนี้เกิดขึ้นได้ แต่ทุกครั้งก็ยังต้องงงอยู่นานว่าทำไมเอาต์พุตไม่ออก ก่อนจะนึกขึ้นได้

  • สำหรับส่วนที่ว่า “ช่วงหลังบทความยาวขึ้นพอสมควร แล้วจะมีใครอยากอ่านบทความ 3000 คำเรื่องบัฟเฟอร์จริง ๆ ไหม” ส่วนตัวแล้วผมอยากอ่าน

    • แล้วแต่บทความ
      ผมคิดว่าบางครั้งบทความยืดยาวก็เป็น น้ำท่วมทุ่ง เพื่อทำ SEO
    • ในส่วนแบบนี้ ผมว่า สรุปด้วย AI ช่วยได้ค่อนข้างมาก ให้มันสรุปบทความแล้วค่อยตรวจทาน
      หรืออาจมีส่วน TLDR กับ NTLDR คือส่วน “ยาวแต่ก็อ่านแล้ว” ก็ได้
  • อยากให้บัฟเฟอร์ทั้งหมดถูกฟลัชทุกครั้งที่ CPU ทั้งระบบเข้าสู่สถานะว่าง
    โดยทั่วไป บัฟเฟอร์เป็นเทคนิคเพื่อประหยัด CPU หาก CPU มีไม่จำกัด บัฟเฟอร์ทั้งหมดก็คงมีขนาด 1 ไบต์ บัฟเฟอร์คือวิธีรวบรวมข้อมูลเพื่อประมวลผลเป็นชุดเพื่อประสิทธิภาพ
    แต่เมื่อ CPU ว่าง ไม่ควรมี “งานที่ไว้ทำทีหลัง” ค้างอยู่ ทันทีที่ scheduler ของเคอร์เนลเข้าสู่สถานะว่าง ควรส่งสัญญาณไปยังทุกโปรเซสว่า ให้ฟลัชบัฟเฟอร์

    • เป็นไอเดียที่น่าสนใจ แต่การส่งสัญญาณไปยังทุกโปรเซสดูแพงมหาศาล
      เท่ากับทำงานทั้งหมดนั้นเสร็จแล้ว ยังบังคับให้ต้องทำ system call เพื่อฟลัชบัฟเฟอร์อีก อาจเพิ่มกลไกให้เคอร์เนลรู้จักบัฟเฟอร์ในพื้นที่ผู้ใช้ แล้วดึงจากตรงนั้นโดยตรงเมื่อว่างก็ได้
      ผมสงสัยว่านี่คล้ายกับ io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff... ในระดับหนึ่งหรือเปล่า
    • เป็นไอเดียเจ๋ง แต่ดูเหมือนไม่ควรทำทั้งหมดพร้อมกันทีเดียว และในระบบใช้พลังงานต่ำ การปลุกโปรเซสที่หลับอยู่แบบคาดเดาอาจทำให้ประสิทธิภาพพลังงานแย่ลงได้
  • บทความนี้สับสนระหว่างสองสิ่งที่ต่างกันคือ ไม่มีบัฟเฟอร์ กับ บัฟเฟอร์ตามบรรทัด
    การไม่มีบัฟเฟอร์ทำให้ประสิทธิภาพแย่ลงโดยไม่จำเป็น และอาจทำให้เอาต์พุตผิดพลาดเมื่อมีหลายแหล่งเขียนไปยัง pipe เดียวกัน บรรทัดที่ยาวพออย่างไรก็จะปนกันอยู่ดี แต่บรรทัดเอาต์พุตส่วนใหญ่ในโลกจริงสั้นกว่า 4096 ไบต์ แม้รวมอักขระจัดรูปแบบ/ควบคุมและอักขระในระนาบเสริมแล้วก็ตาม
    บัฟเฟอร์ตามบรรทัดเป็นค่าเริ่มต้นของเทอร์มินัล และมักเป็นพฤติกรรมที่ต้องการใน pipe ด้วย ให้รันแต่ละคำสั่งภายใต้ stdbuf -oL -eL ก็พอ โปรแกรมหายากที่ต้องการอัปเดตภายในบรรทัดก็ต้องฟลัชเองอยู่แล้ว ดังนั้นตรงนี้ก็จะทำงานได้ถูกต้อง
    สิ่งที่ stdbuf ทำจริง ๆ ดูได้ประมาณนี้:
    env -i \command -v stdbuf` -oL -eL `command -v env``

  • เคยเขียนเกี่ยวกับปัญหานี้ไว้ก่อนหน้านี้: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
    สำหรับคำสั่งที่ไม่ทำ buffering นั้นอาจขึ้นกับ implementation หรือในกรณีของ 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 จะสามารถบังคับข้อจำกัดแบบนั้นกับโปรเซสที่เขียนได้ ยกเว้นกรณีใช้แฮ็กหนัก ๆ อย่าง ptrace()
      อาจจะปรับขนาด pipe buffer ได้ แต่ไม่รู้ว่ามีธรรมเนียมใดที่บอกว่า standard C I/O ต้องทำตามสิ่งนี้
      อย่างไรก็ตาม ในกรณีนี้ดูเหมือน stdbuf จะไม่ช่วย:
      $ ./a | stdbuf -i0 -- cat
      #include
      #include
      int main(void) {
      for (;;) {
      printf("n");
      usleep(100000);
      }
      }
  • มีเหตุผลที่ดีที่ buffer มีอยู่ การพิมพ์ output ลงหน้าจอช้ามากเมื่อเทียบกับการเขียนลง buffer
    การ output ทีละตัวอักษรนั้นไม่มีประสิทธิภาพอย่างมาก
    เป็นปัญหาเก่าแก่ และมักเจอเมื่อจัดการกับ UART วิธีแก้ที่เป็นไปได้มีหลายแบบ: แบบอิงบรรทัดที่ทำเครื่องหมายจบ output ด้วยอักขระพิเศษอย่าง newline, แบบอิงความยาวที่รอจนกว่าจะถึงความยาวอย่าง 8KB, และแบบอิงเวลาที่ output ทุก ๆ X มิลลิวินาที
    แต่ละวิธีมีข้อดีข้อเสีย และอะไรเหมาะที่สุดก็ขึ้นกับแอปพลิเคชัน ผมคิดว่าส่วนในบทความที่บอกว่าโปรแกรมบางตัวไม่ใช้ buffering นั้นผิด โปรแกรมเหล่านั้นเพียงแค่ไม่ได้ใช้ แบบอิงความยาว ที่เห็นได้ชัดเท่านั้น

    • จะทำงานได้ดีที่สุดเมื่อสามารถรู้ข้อจำกัดได้จากชั้นที่อยู่เหนือ interface ขึ้นไปหนึ่งหรือสองชั้น
      แนวทางแบบอิงบรรทัดเป็นแบบนั้น แต่ต้องมีข้อตกลงว่าจะใช้อักขระใด ปกติก็คือ newline
    • ไม่ใช่แค่ปัญหาต้นทุนของการให้ backend จัดการการเขียนจริงเท่านั้น การทำ system call จำนวนมากขนาดนั้นไปยัง /dev/null ก็ทำให้ประสิทธิภาพตกลงอย่างมากได้
  • ใช้ Unix มานานกว่า 35 ปีแล้ว แต่ไม่เคยเข้าใจอย่างถ่องแท้ว่าสิ่งนี้ทำงานอย่างไร
    ดีที่ได้คำอธิบายโดยรวมเกี่ยวกับพฤติกรรม buffering ที่ครอบคลุมหลายระบบและหลายองค์ประกอบ และได้เรียนรู้อะไรแน่นอน

  • สำหรับส่วนที่ว่า “เมื่อกด Ctrl-C ใน pipe เนื้อหาใน buffer จะหายไป” ผมคิดว่าโปรแกรมส่วนใหญ่น่าจะ flush buffer เมื่อเจอ SIGINT
    แต่ถ้าจะให้ shell ทำงานแบบนั้น คงต้องส่ง SIGINT ไปเฉพาะโปรแกรมแรกของ pipeline เท่านั้น ซึ่งดูเหมือนพฤติกรรมจริงอาจจะไม่ใช่อย่างนั้น

    • จำได้ว่าโปรเซสสุดท้ายจะได้รับ sigint และที่เหลือจะได้รับ sigpipe