- คิดว่าผู้คนยังตระหนักกันไม่มากพอว่าเอกสารของ Linux kernel API นั้นไม่สมบูรณ์แค่ไหน และ Rust ช่วยแก้ปัญหาส่วนนี้ได้อย่างไร
- ได้เขียน Rust abstraction สำหรับหลายซับซิสเต็ม แต่แทบทุกครั้งก็ยังต้องอ่านซอร์สโค้ด C เพื่อให้เข้าใจอย่างครบถ้วนว่าจะใช้งาน API อย่างปลอดภัยได้อย่างไร
- จากเพียงฟังก์ชันซิกเนเจอร์กับคอมเมนต์เอกสารหรือเอกสารที่ระบุไว้ชัดเจน ยังยากที่จะเข้าใจได้ครบถ้วนว่าควรใช้ API อย่างปลอดภัยอย่างไร
- ต้องถือ lock ค้างไว้หรือไม่
- อาร์กิวเมนต์ reference count เป็นการส่งต่อ reference หรือเป็นการรับ ownership ของ reference นั้นเอง
- ตอนเรียก callback ยังถือ lock อยู่หรือว่าต้องไป acquire เอง
- มีอะไรพิเศษเกี่ยวกับ free callback หรือไม่
- ลำดับ lock ที่ตั้งใจไว้คืออะไร
- มีสถานการณ์พิเศษที่บาง operation สามารถใช้ lock ได้เป็นกรณี ๆ ไปหรือไม่
- ยอมรับอาร์กิวเมนต์ NULL หรือไม่ และใช้อย่างถูกต้องได้แบบไหน
- ในกรณี error จะเกิดอะไรขึ้นกับ reference count
- pointer ที่คืนกลับมาพร้อม reference count นั้นถูกเพิ่มค่าไว้แล้ว หรือเป็นเพียงการยืมโดยนัยจาก reference ของอาร์กิวเมนต์ที่ส่งเข้าไป
- ค่าที่คืนกลับมาเป็น pointer ที่ใช้ได้เสมอ, อาจเป็น NULL, หรืออาจเป็น ERR_PTR ได้
- pointer ที่คืนผ่านอาร์กิวเมนต์ทางอ้อมจะถูกเคลียร์เป็น NULL เมื่อเกิด error หรือปล่อยค้างไว้
- ถ้าไม่ต้องการ pointer ที่คืนกลับมา การส่ง NULL ** จะถือว่าใช้ได้หรือไม่
- บางครั้งข้อกำหนดก็สมเหตุสมผล แต่ไม่ได้มีการบันทึกไว้
- บางครั้งข้อกำหนดยืดหยุ่นหรือซับซ้อนเกินไป จนเวลาจะเขียน Rust abstraction ต้องตัดสินใจเชิงอัตวิสัยเพื่อบีบให้เหลือรูปแบบการใช้งานที่ปลอดภัย
- บางครั้งต้องเพิ่ม lock ภายใน abstraction เพื่อให้ปลอดภัย
- บางครั้งต้องปรับ C code เล็กน้อยเพื่อให้เป็นแบบ orthogonal, มีเหตุผลมากขึ้น และใช้งานง่ายขึ้น (เช่น เปิดเผยฟังก์ชันปลด lock สำหรับใช้ในสถานะที่ล็อกอยู่)
- บางครั้งเรื่อง lock ละเอียดอ่อนมากจนแม้จะเขียน Rust abstraction ที่ปลอดภัยได้ แต่ก็ยังต้องมีคอมเมนต์เอกสารยาว ๆ ว่าต้องระวังลำดับการใช้งานและการปล่อย เพื่อหลีกเลี่ยง deadlock (Rust ไม่ได้ป้องกัน deadlock โดยเนื้อแท้)
- บางครั้งก็แก้ไม่ได้เลยหากไม่ทำให้ C code สมเหตุสมผลขึ้นกว่านี้ (กรณีของ
drm_sched)
- อย่างไรก็ตาม ในกรณีส่วนใหญ่ การประนีประนอมที่เกิดขึ้นระหว่างการเขียน Rust abstraction ก็มักชี้ให้เห็นถึงปัญหาในการออกแบบ C code และแนวทางในการปรับปรุง
- แนวทางทั่วไปคือ "เขียน Rust code ก่อนโดยแก้ C code ให้น้อยที่สุดเพื่อเลี่ยงแรงปะทะ แล้วค่อยใช้บทเรียนที่ได้มาเสนอการเปลี่ยนแปลง C code" (ตอนนี้ยังไปไม่ถึงส่วนที่สอง)
- ผลลัพธ์คือ ในกรณีส่วนใหญ่ดูแค่ Rust API ก็พอจะรู้วิธีใช้อย่างถูกต้องได้
- ไม่ต้องกังวลเรื่อง reference count, NULL pointer, การลืมตรวจผลลัพธ์, หรือการปล่อย reference เมื่อเกิด error
- ไม่ต้องกังวลเรื่องการใช้ lock ให้ถูกต้อง, การลืม acquire reference, หรือการ free ซ้ำ
- ไม่ต้องมานั่งสงสัยว่าค่า error return ถูกเข้ารหัสมาอย่างไร
- เพราะถ้าทำสิ่งเหล่านี้ผิด โค้ดจะคอมไพล์ไม่ผ่าน
- แน่นอนว่ายังใช้งาน API ผิดได้อยู่ แต่กรณีเลวร้ายที่สุดก็มักเป็น error return หรือ deadlock เท่านั้น (deadlock ดีบักได้ง่ายด้วย lockdep และการผสานกับ
Arc<>สามารถจับข้อผิดพลาดเรื่อง lock ที่เกี่ยวกับการ free/การลด reference ได้)
- แม้แต่ OpenFirmware/DeviceTree API ที่ถือว่ามีเอกสารค่อนข้างเข้มงวดแล้ว ใน C ก็ยังน่าเบื่อและผิดพลาดได้ง่ายที่จะทำตามกฎทั้งหมด
- ดูจาก OF code ในไดรเวอร์แล้วมีโอกาสเกิด reference leak สูง
- ระบบส่วนใหญ่ไม่ได้คอมไพล์เคอร์เนลด้วย OF_DYNAMIC จึงมองข้าม reference count และทำให้ปัญหานี้ไม่ถูกค้นพบหรือแก้ไข
- แต่ OF Rust abstraction ที่ฉันเขียนจัดการ reference counting ให้อัตโนมัติ จึงไม่ต้องคอยกังวลเรื่องนี้
- ข้อดีของการเขียน kernel ด้วย Rust เมื่อเทียบกับ C
- เวลาจะเขียน kernel ด้วย C มีทางเลือกอยู่แค่สองแบบ
- ลองเขียนไปก่อนแล้วหวังว่า reviewer จะช่วยจับได้ หรือไม่ก็ไปทุกข์กับการดีบัก
- หรือไม่ก็ต้องใช้เวลาหลายชั่วโมงทำความเข้าใจทุกอย่างก่อนจึงค่อยกล้าใช้โค้ด และหวังว่าจะจับได้ครบทุกจุด
- สิ่งนี้ยังเพิ่มภาระงานให้ reviewer และ maintainer ด้วย
- เพราะต้องตรวจ submission เพื่อดูว่าปฏิบัติตามกฎซ่อนเร้นที่ไม่มีเอกสารครบทุกข้อหรือไม่
- บางครั้งก็พลาดปัญหา บางครั้งปัญหาใหญ่จนต้องรีแฟกเตอร์โค้ดครั้งใหญ่
- ใน Rust สิ่งเหล่านี้หายไปหมด ถ้าคอมไพล์ผ่านก็ปลอดภัย และไม่มีการทำงานผิดพลาดหรือ reference leak (ยกเว้นโค้ด
unsafeแต่ก็ต้องรีวิวเฉพาะส่วนนั้น และมีกฎว่าควรมีเอกสารที่ดี)- แน่นอนว่ายังต้องมี code review และความช่วยเหลือจากผู้เชี่ยวชาญของแต่ละซับซิสเต็มอยู่ดี Rust ไม่ได้เสกให้โค้ดสมบูรณ์แบบได้เอง
- แต่ Rust ช่วยตัดปัญหาและความผิดพลาดระดับล่างที่น่าปวดหัวออกไป ทำให้โฟกัสกับปัญหาระดับสูงได้
- มุมมองต่อบรรดา Linux developers
- ฉันไม่ได้ตำหนินักพัฒนา Linux เรื่องเอกสารที่ไม่สมบูรณ์
- Linux kernel ซับซ้อนมากและต้องรับมือกับรายละเอียดอ่อน ๆ จำนวนมาก
- API ฝั่ง user space ส่วนใหญ่มีกฎที่ง่ายกว่ามากในการใช้งานอย่างปลอดภัย
- เคอร์เนลนั้นยาก
- แม้แต่นักพัฒนาเคอร์เนลที่มีประสบการณ์ก็ยังทำพลาดกับเรื่องพวกนี้อยู่เสมอ
- มันไม่ใช่ปัญหาเรื่องทักษะ แต่เป็นเพราะมนุษย์ไม่อาจเก็บกฎซับซ้อนทั้งหมดนี้ไว้ในหัวและทำได้ถูกต้องทุกครั้ง
- ทางออก
- เราต้องการเครื่องมือ
- ทางออกคือ Rust เมื่อเข้ารหัสกฎทั้งหมดไว้ในโค้ดและ type system ครั้งเดียว ก็ไม่ต้องกังวลกับมันอีก
- คล้ายกับที่ทางออกของการถกเถียงเรื่อง coding style คือเข้ารหัสกฎทั้งหมดไว้ในตัวจัดรูปแบบอัตโนมัติ
- จากนั้นเราก็จะเลิกกังวลกับปัญหาความปลอดภัยระดับล่าง, ownership และ synchronization แล้วหันไปกังวลกับสิ่งสำคัญกว่าอย่างการออกแบบไดรเวอร์และซับซิสเต็มระดับสูงได้
- การจัดรูปแบบโค้ดในโครงการ Rust for Linux
- โครงการ Rust for Linux ใช้
rustfmtกับ submission จริง ๆ - เวลาจะเขียน kernel Rust ก็ไม่ต้องกังวลเรื่องการจัดรูปแบบโค้ดหรือคำบ่นใน code review
- แค่รัน
make rustfmtก็พอ
- โครงการ Rust for Linux ใช้
ความเห็นของ GN⁺
- บทความนี้ชี้ปัญหาเรื่องเอกสาร API และความปลอดภัยในการพัฒนา Linux kernel ได้ชัดเจน และแสดงให้เห็นข้อจำกัดของภาษา C กับข้อดีของ Rust ได้ดี
- อย่างไรก็ตาม การบอกว่า 'Rust คือทางออกเดียว' อาจดูเกินจริงไปบ้าง เพราะยังมีวิธีอื่นอย่างเครื่องมือ static analysis ที่ช่วยปรับปรุงบางส่วนได้
- แม้ Rust จะช่วยแก้ปัญหาได้มาก โดยเฉพาะด้าน memory safety แต่ก็ยังต้องมี code review และการทดสอบอย่างรอบคอบอยู่ดี มันไม่ใช่ silver bullet
- การย้ายไปใช้ Rust อาจมีความยากหลายด้าน เช่น ความเข้ากันได้กับ C code เดิม และเส้นโค้งการเรียนรู้ของนักพัฒนา ดังนั้นการค่อย ๆ นำมาใช้ทีละขั้นน่าจะเหมาะสมกว่า
- หากจะปรับปรุงแนวปฏิบัติและวัฒนธรรมเก่าแก่ของ Linux kernel ให้ดีขึ้น ก็น่าจะต้องอาศัยความพยายามหลายด้านนอกเหนือจาก Rust เช่น การทำเอกสาร, การมีพี่เลี้ยง, และการสื่อสาร
- โดยรวมแล้ว บทความนี้แสดงให้เห็นศักยภาพและข้อดีของ Rust ในการพัฒนา Linux kernel ได้ดี ขณะเดียวกันก็เตือนให้ระวังความคาดหวังที่มากเกินไปหรือการเชื่อแบบสุดโต่ง มุมมองจึงค่อนข้างสมดุล แม้การนำ Rust เข้ามาใช้จะมีความยากทั้งเชิงเทคนิคและเชิงวัฒนธรรม แต่ในระยะยาวก็น่าจะช่วยเพิ่มความปลอดภัยและความสามารถในการบำรุงรักษาของ kernel code ได้
2 ความคิดเห็น
Rust... ส่วนตัวก็เคยลองศึกษาดูอยู่บ้าง แต่ในบริษัทของเรายังไม่ได้นำมาใช้เลยครับ เพราะมีโค้ดที่เขียนด้วย C++ อยู่แล้วกองโต และยังมีปัญหาที่บุคลากรเดิมต้องกลับไปเรียนรู้ rust ใหม่อีก... ได้ยินมาว่าที่เกาหลีก็มีบริษัทที่นำ rust ไปใช้ในโปรดักชันอยู่แล้วเหมือนกัน ถ้ามีการแบ่งปันประสบการณ์อะไรที่เกี่ยวข้องก็น่าจะดีครับ
ความคิดเห็นใน Hacker News
ภาษาอย่าง Rust และ Swift มีความสามารถในการแสดงเจตนาได้สูง ทำให้คอมไพเลอร์บอกได้ว่าชนิดข้อมูลหรือเมธอดนั้นปลอดภัยต่อเธรดหรือไม่
ไลบรารี Rust หลายตัวมักมีเอกสารไม่เพียงพอ
พยายามใช้ Rust แบบเดียวกับ C แล้วเจอปัญหากับตัวตรวจสอบการยืม
&selfหรือ&mut selfเป็นเรื่องสำคัญ&mut selfและต้องการแชร์อินสแตนซ์ข้ามเธรด ก็ต้องใช้ mutexเมื่อดู API ของ Rust ส่วนใหญ่แล้วจะรู้ได้ว่าควรใช้อย่างไรให้ถูกต้อง
ตัวอย่างที่ชัดเจนของ Rust คือแนวทางการใช้ lock เพื่อปกป้องข้อมูล
ในภาษาอื่น ๆ หากทำ API แบบซ้ำซ้อนก็ช่วยเพิ่มความชัดเจนของโค้ดและเอกสารได้
เวลาทำ Python extension ด้วย C มักมีปัญหาว่าต้องรู้ calling convention
คนเหล่านี้คือฮีโร่และทำงานได้ยอดเยี่ยม
เราเข้าใกล้การทำโค้ดที่อธิบายตัวเองได้อย่างสมบูรณ์ไปอีกขั้น