บัก หนึ่งในล้านลูป ทำซ้ำไม่ได้ เกิดอย่างไร !
Last Updated on Monday, 31 December 2012 23:14 Written by Administrator Monday, 31 December 2012 22:23
บักชนิดหนึ่งในหลายล้านลูป ที่อาจเกิดกับการเขียนโปรแกรมระดับ Hardware
เคยเจอมั้ย โปรแกรมใน MCU ทำงานเป็นอย่างดีเป็นเวลาหลายนาที หรืออาจจะหลายวัน แล้วอยู่ดีๆ ก็ บรึ้มม เกิด error ขึ้น โดยไม่รู้ว่ามันมาจากไหน และทำซ้ำให้เกิดอีกก็ไม่ได้ มันเกิดแบบไร้เงื่อนไข นึกจะเกิดก็เกิด มันมีกลไกอย่างไร? จะแก้อย่างไร? การทำงานง่ายๆ อย่าง count++; สั้นๆ ดูเหมือนจะไม่มีพิษสงอะไรเลย ลองดูนี่กันว่ามันทำอะไรวายป่วงได้บ้าง...
ผมได้ทำโปรแกรมง่ายๆ นี้ผมเพิ่มเข้าไปในงาน หลังจากที่สงสัยว่า MCU มันทำงานได้เร็วแค่ไหนนะ และงานที่เราประมวลผลมันใช้พลังการประมวลแค่ไหน ผมทำโดยการนับลูปเมนว่ามันวิ่งได้กี่รอบในหนึ่งวินาที โดยใช้ 1ms clock tick interrupt จำนวน 1000 ลูป มาเป็นฐานเวลาหนึ่งวินาที และเทียบกับการทำงานร้อยเปอร์เซนต์ ซึ่งอิงจากการถูกโหลดจนเหลือ 1000 ลูปเมนใน 1 วินาที เพราะว่า scheduler ทำงานทุก 1 ms หรือ 1000 รอบ ในหนึ่งวินาที พอดี
สูตรที่ใช้คือ
(1000 / LoopCount ) x 100 = CPU Load (%)
ในโปรแกรมถูกย่นย่อ และสเกลค่าให้แสดงเป็นทศนิยมได้ด้วย สูตรก็จะเป็น..
1000000/LoopCount = CPU Load (10x %)
เอามาหารสิบ ก็ได้เปอร์เซนต์ออกมา เศษที่เหลือ ก็เป็นทศนิยมหนึ่งตำแหน่ง จบ
คำสั่งที่ increment ค่า อยู่ใน Main Loop คือ... RuntimeLoopCount++; // ง่ายๆ เลย! แล้วเมื่อครบเวลาหนึ่งวินาที ค่าที่นับนี้ถูกเซฟเก็บโดยอินเตอรรัพท์ และล้างค่า RuntimeLoopCount เป็นศูนย์
ดูเหมือนไม่มีอะไรใช่มะ มันบวกไปเรื่อยๆ ถึงเวลาก็เก็บค่า รีเซ็ทใหม่ วนไปทุกวินาที
แต่ผลการทำงานให้ค่าที่ผิดปกตินานๆ ครั้ง บางครั้งเป็นสิบนาทีกว่าจะโผล่มาให้เห็น ค่าที่ได้น้อยกว่าความจริงประมาณครึ่งหนึ่งพอดี ทำไมมันต้องครึ่งพอดีด้วยนะ แปลกนะนี่ !! นึกออกมั้ยว่าทำไม?
ผมรู้ว่ามันเกิดเพราะ interrupt แน่ๆ เลยครอบบรรทัดนี้ด้วยคู่ของคำสั่ง Disable interrupt และ Enable interrupt ผลคือ ไม่เกิด error อีกเลย
คราวนี้เรามาดูกันว่ามันเกิดอะไรขึ้นกับคำสั่ง Increment ตัวแปร ง่ายๆ อย่างนี้..
รูปนี้คือโปรแกรมส่วนของอินเตอรรัพท์ และฟังก์ชั่นที่รันในเมนลูป จะเห็นว่าที่ผมอธิบายการทำงานมาข้างต้นมันทำงานอย่างไร ซึ่งผมได้ครอบส่วนนับจำนวนลูปด้วยคำสั่ง Disable interrupt และ Enable interrupt ไว้แล้ว
และนี่ก็คือ Disassembly ของคำสั่งว่าการ increment มันประกอบด้วยอะไรบ้าง คุณจะเห็นว่ามันแบ่งเป็นสามคำสั่งแมชชีนโค้ด นั่นคือโหลดค่า เพิ่มค่า และเซฟค่า
ระหว่างการโหลดค่า จนถึงเซฟค่า หากมีการ interrupt เกิดขึ้น และเปลี่ยนค่าในหน่วยความจำไป เมื่อกลับจากอินเตอรัพท์ ค่าจะถูกเซฟทับเป็นครั้งที่สองกลับเป็นค่าเก่า(ที่ถูกบวกด้วยหนึ่ง)ก่อนอินเตอรรัพท์ทันที หมายความว่าการเคลียร์ค่าที่เกิดขึ้นในอินเตอรัพท์ไม่มีผลใดๆ การทำงานคือนับค่าต่อเนื่องไปในอีกรอบหนึ่งวินาทีถัดมา ทำให้ได้ค่าจำนวนลูปเมนมากกว่าปกติสองเท่า
เมื่อค่าที่นับได้มากกว่าปกติสองเท่า ถูกนำมาแสดงผลในวินาทีถัดมา ผลคือค่าของ CPU Load ที่คำนวณได้ ผิดไปโดยให้ผลน้อยกว่าความเป็นจริงครึ่งหนึ่งพอดี
นี่คือข้อผิดพลาดที่ได้จากผลลัพธ์ โดยไม่ได้ปิดอินเตอร์รัพท์ระหว่างการ Increment ค่า
ส่วนภาพนี้ คือผลที่ถูกต้องหลังจากปิดอินเตอรรัพท์ระหว่างการ increment ค่าแล้ว ไม่เกิดอินเตอรรัพท์ระหว่าการโหลด และเซฟค่า จึงไม่เกิดข้อผิดพลาดอีกต่อไป
การทำงานของอินเตอรัพท์ มันเกิดที่ไหนก็ได้บนโปรแกรม หากว่าโปรแกรมหลักกำลังทำงานกับค่าตัวแปรนึง และมีการเปลี่ยนค่านั้นโดยอินเตอรัพท์อีกด้วย เราต้องคิดด้วยว่าถ้ามันถูกเปลี่ยนระหว่างที่โปรแกรมหลักยังทำงานกับค่านั้นไม่เสร็จจะเกิดอะไรขึ้น
ผมทำงานกับ MCU มาก็นานมากละ ตั้งแต่เขียน Assembly มาจนถึงเขียนด้วย C การเขียนอินเตอรรัพท์เนี่ย ไอ้ตอนเขียนด้วย Assembly เราจะเห็นหมดละ ว่าปัญหาอาจเกิดตรงไหนได้บ้าง เพราะเราเห็นถึงขั้น Atomic หรือระดับการประมวลแต่ละแมชชีนโค้ด
การเขียนด้วย Assembly จะเห็นชัดว่าเราเริ่มตรงไหน จบตรงไหน แต่พอมาเขียนด้วย C เราก็จะมองไม่เห็นแล้วว่าแต่ละบรรทัดมันกลายเป็นแมชชีนโค้ดอย่างไร ในหนึ่งบรรทัดง่ายๆ มีการอินเตอรัพท์ผ่ากลางได้ไหม ต้องคิดให้รอบคอบ และตรวจให้ดีด้วย
ปล. ตัวแปรที่ถูกเปลี่ยนค่ากลางอากาศโดยอินเตอรรัพท์แบบนี้ ต้องมี modifier (เอ๊ะ เขาเรียกแบบนี้หนือเปล่านะ?) "volatile" เข้าไปข้างหน้าด้วยเมื่อ declare ตัวแปรนี้ เช่น..
volatile unsigned int RuntimeLoopCount;
ด้วยนะ ไม่งั้น optimizer ของ compiler จะนึกว่ามันเป็นค่าคงที่ ไม่มีใครมาเปลี่ยนค่ามันกลางอากาศได้ ผลคือรูปแบบ machine code จะเปลี่ยนไปและใช้งานไม่ได้อย่างที่ตั้งใจ