Heisenbug อีกหนึ่งตัวผ่านจระเข้ไป

Heisenbug อีกหนึ่งตัวผ่านจระเข้ไป

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

ที่นี่ fortune โปรแกรมแบบมีเงื่อนไขโดยไม่ต้อง exit(rand()).

คุณสามารถอธิบาย? เกิดอะไรขึ้นที่นี่?

การพูดนอกเรื่องโคลงสั้น ๆ - ประวัติศาสตร์

ฉันคุ้นเคยกับ Heisenbug นี้เป็นครั้งแรกเมื่อหนึ่งในสี่ของศตวรรษที่ผ่านมา จากนั้นสำหรับเกตเวย์ใน FaxNET จำเป็นต้องสร้างยูทิลิตี้หลายอย่างผ่าน ท่อ "การเล่นหมากฮอส" ภายใต้ FreeBSD ตามที่คาดไว้ ฉันคิดว่าตัวเองเป็นโปรแกรมเมอร์ขั้นสูงและมีประสบการณ์พอสมควร ดังนั้นฉันจึงตั้งใจทำทุกอย่างอย่างรอบคอบและระมัดระวังที่สุดเท่าที่จะเป็นไปได้ โดยให้ความสนใจเป็นพิเศษกับการจัดการข้อผิดพลาด...

ประสบการณ์ก่อนหน้าของฉันในการจัดการกับจุดบกพร่องใน sendmail และ uucp/uupc ได้เพิ่มความขยันหมั่นเพียรของฉันในเรื่อง “การจัดการข้อผิดพลาดอย่างละเอียด” ไม่มีประโยชน์ที่จะเจาะลึกรายละเอียดของเรื่องราวนั้น แต่ฉันต่อสู้กับ Heisenbug นี้เป็นเวลาสองสัปดาห์เป็นเวลา 10-14 ชั่วโมง จึงจำขึ้นใจและเมื่อวานคนรู้จักเก่าคนนี้ก็แวะมาเยี่ยมอีกครั้ง

TL; DR คำตอบ

คุณประโยชน์ head สามารถ ปิดช่องจาก fortune ทันที ทันทีที่เขาอ่านบรรทัดแรก ถ้า fortune เอาต์พุตมากกว่าหนึ่งบรรทัด จากนั้นจึงเป็นการโทรที่สอดคล้องกัน write() จะส่งคืนข้อผิดพลาดหรือรายงานว่าเอาต์พุตมีจำนวนไบต์น้อยกว่าที่ร้องขอ ในทางกลับกันเขียนด้วยการจัดการข้อผิดพลาดอย่างระมัดระวัง fortune มีสิทธิที่จะสะท้อนสถานการณ์นี้ในสถานะทางออก แล้วเนื่องจากการติดตั้ง set -o pipefail จะทำงาน || echo "Вы проиграли".

อย่างไรก็ตาม head อาจจะมาไม่ทัน ปิดก่อน fortune จะเสร็จสิ้นการส่งออกข้อมูล จากนั้นมันจะทำงาน && echo "Повезло!".

หนึ่งในวันนี้ของฉัน GNUMakefile มีดังกล่าว ชิ้นส่วน:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

แปลเป็นมนุษย์แล้ว

เป็นเรื่องปกติสำหรับที่นี่ GNU ทำ и ทุบตี ในวิธีคอมไพเลอร์โดยใช้ตัวเลือก --version มันจะถามว่าเขาเป็นใคร และหากตัวเลือกนี้ไม่ได้รับการสนับสนุน ก็จะแทรกต้นขั้วเข้าไป "โปรดใช้คอมไพเลอร์ที่เข้ากันได้กับ GCC หรือ CLANG".

เช่น ต้นแบบ สามารถพบได้ทุกที่ มันปรากฏตัวในสถานที่นี้เมื่อนานมาแล้วและทำงานได้อย่างสมบูรณ์แบบทุกที่ (Linux, Solaris, OSX, FreeBSD, WSL ฯลฯ) แต่เมื่อวานใน altlinux บนแพลตฟอร์ม เอลบรุส 2000 (E2K) ฉันสังเกตเห็น:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

พูดตามตรงว่าฉันจำ "คนรู้จัก" เก่าของฉันในทันทีไม่ได้ นอกจากนี้ โปรเจ็กต์นี้ได้รับการทดสอบหลายครั้งบน Elbrus และภายใต้การแจกแจงที่แตกต่างกันมากมาย รวมถึง Alt ด้วยคอมไพเลอร์ที่หลากหลาย เวอร์ชันของ GNU Make และ bash ดังนั้นฉันไม่ต้องการที่จะเห็นความผิดพลาดของฉันที่นี่

เมื่อพยายามจำลองปัญหาและ/หรือทำความเข้าใจกับสิ่งที่เกิดขึ้น สิ่งแปลกๆ ก็เริ่มเกิดขึ้น
สะกดบรรทัดคำสั่ง:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

บางครั้งมันจะสร้างข้อความเพิ่มเติม แต่ก็ไม่... บ่อยครั้งที่ตัวเลือกใดตัวเลือกหนึ่งจะอยู่ได้ค่อนข้างนาน แต่ถ้าคุณกระตุ้นนานกว่านี้ คุณจะได้ทั้งสองอย่างเสมอ!

แน่นอน strace ทุกอย่างของเรา! และเมื่อพิมพ์คำด่า strace แต่ไม่มีเวลากด Enter ฉันจำเพื่อนเก่าของฉัน Mr. Heisenbug และนักพัฒนาได้ คอมไพเลอร์ ตัวฉันเองเมื่อ 25 ปีที่แล้ว ความคิดถึง… และฉันตัดสินใจที่จะเศร้าและเขียนบันทึกนี้ 😉

โดยวิธีการเช่นเดียวกับการเคารพตนเอง ไฮเซนบั๊ก, ภายใต้ strace ไม่ชอบที่จะสืบพันธุ์

เกิดอะไรขึ้น?

  • คุณประโยชน์ head มีสิทธิ์ (หรือมากกว่านั้นคือถูกบังคับ) เพื่อปิดช่องที่กำลังอ่านทันทีที่อ่านตามจำนวนบรรทัดที่ร้องขอ
  • ผู้เขียนโปรแกรมสร้างข้อมูล (ในกรณีนี้คือ cc) สามารถ พิมพ์หลายบรรทัดและ ฟรี ทำสิ่งนี้ผ่านการโทรหลายครั้ง write().
  • ถ้า ผู้อ่านจะมีเวลาปิดช่องฝั่งตนก่อนจบการบันทึกฝั่งผู้เขียน จากนั้น ผู้เขียนจะได้รับข้อผิดพลาด
  • โปรแกรมนักเขียน สิทธิที่จะ ทั้งสองละเว้นข้อผิดพลาดในการเขียนช่องและสะท้อนให้เห็นในโค้ดที่สมบูรณ์ของคุณ
  • เนื่องจากการติดตั้ง set -o pipefail รหัสการเสร็จสิ้นไปป์ไลน์จะไม่เป็นศูนย์ (ผิดพลาด) หากผลลัพธ์ไม่เป็นศูนย์จากองค์ประกอบอย่างน้อยหนึ่งรายการ จากนั้นจึงจะทำงานได้ || echo "Please use GCC or CLANG compatible compiler".

อาจมีการเปลี่ยนแปลงได้ขึ้นอยู่กับว่าโปรแกรมตัวเขียนทำงานกับสัญญาณอย่างไร ตัวอย่างเช่น โปรแกรมอาจยุติการทำงานอย่างผิดปกติ (ด้วยการสร้างสถานะการยกเลิกที่ไม่ใช่ศูนย์/ข้อผิดพลาดโดยอัตโนมัติ) หรือ write() จะส่งคืนผลลัพธ์ของการเขียนไบต์น้อยกว่าที่ร้องขอและตั้งค่า errno = EPIPE.

ใครผิด?

ในกรณีที่อธิบายไว้ เล็กน้อยของทุกสิ่ง. เกิดข้อผิดพลาดในการจัดการใน cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) ไม่ใช่ ซ้ำซ้อน ในหลายกรณี เป็นการดีกว่าถ้าทำผิดโดยระมัดระวัง แม้ว่าจะเผยให้เห็นข้อบกพร่องอย่างกะทันหันในสำเร็จรูปที่ออกแบบมาเพื่อพฤติกรรมแบบดั้งเดิมก็ตาม

จะทำอย่างไร?

ไม่ถูก:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

แก้ไข:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    ที่นี่ การปิดไปป์ก่อนกำหนดจะได้รับการจัดการโดยล่ามคำสั่ง เมื่อให้บริการไปป์ไลน์ที่ซ้อนกัน ("ภายใน" วงเล็บ) ตามนั้น ถ้า fortune จะรายงานข้อผิดพลาดเป็นลายลักษณ์อักษรไปยังช่องปิดในสถานะแล้วเอาต์พุต || echo "Ошибка" ไปไม่ถึงไหนเพราะปิดช่องไปแล้ว

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    นี่คือยูทิลิตี้ cat ทำหน้าที่เป็นแดมเปอร์เพราะจะละเว้นข้อผิดพลาด EPIPE เมื่อถอนตัว แค่นี้ก็เพียงพอแล้วสำหรับการสรุป fortune เล็ก (หลายบรรทัด) และพอดีกับบัฟเฟอร์ช่องสัญญาณ (ตั้งแต่ 512 ไบต์ถึง µ64K ใน OS ส่วนใหญ่ ≥4K) มิฉะนั้นปัญหาอาจกลับมา

วิธีการประมวลผลอย่างถูกต้อง EPIPE และข้อผิดพลาดในการบันทึกอื่น ๆ ?

ไม่มีวิธีแก้ปัญหาที่ถูกต้องเพียงวิธีเดียว แต่มีคำแนะนำง่ายๆ:

  • EPIPE ต้อง จะต้องได้รับการประมวลผล (และสะท้อนให้เห็นในสถานะการออก) เมื่อส่งออกข้อมูลที่ต้องใช้ความสมบูรณ์ ตัวอย่างเช่นระหว่างการทำงานของผู้จัดเก็บหรือยูทิลิตี้สำรองข้อมูล
  • EPIPE ดีกว่าที่จะเพิกเฉย เมื่อแสดงข้อมูลและข้อความเสริม เช่น เมื่อแสดงข้อมูลเกี่ยวกับตัวเลือกต่างๆ --help หรือ --version.
  • หากโค้ดที่กำลังพัฒนาสามารถนำไปใช้ในไปป์ไลน์ก่อนได้ | headแล้ว EPIPE เป็นการดีกว่าที่จะเพิกเฉย ไม่เช่นนั้นจะเป็นการดีกว่าที่จะประมวลผลและสะท้อนถึงสถานะการออก

ผมขอใช้โอกาสนี้แสดงความขอบคุณทีมงาน อสมท и altlinux เพื่อผลงานอันทรงประสิทธิภาพอันยิ่งใหญ่ ความมุ่งมั่นของคุณน่าทึ่งมาก!
สู้ต่อไป สู้ ๆ นะ การประชุมในฤดูใบไม้ร่วง!

ขอบคุณ เบเรซ เพื่อแก้ไขคำผิดและข้อผิดพลาด
เคดีพีวีจาก จอร์จี้ เอ.

ที่มา: will.com

เพิ่มความคิดเห็น