การศึกษาลินุกซ์เคอร์เนล ด้วย Raspberry Pi: พื้นฐานการทำงานของลินุกซ์ดีไวซ์ไดร์เวอร์ (ตอนที่ 1)

By | December 5, 2016

เกริ่นนำนิดหน่อย

ตั้งใจจะเขียนเรื่อง การเขียนลินุกซ์ดีไวซ์ไดร์เวอร์ บนบอร์ด ราสเบอรรี่พาย มานานแล้ว แต่พอมาลองเขียนเอาจริงๆ ก็เริ่มรู้สึกว่าถ้าไม่มีการปูพื้นเรื่องที่เกี่ยวข้องให้ก่อน มือใหม่ที่พึ่งเริ่มศึกษาก็คงจะเข้าใจยากแน่ๆ และเรื่องที่จะเขียนนี้ก็คงไม่มีประโยชน์กับมือเก่าที่ได้ศึกษาโดยตรงกับ Textbook หรือ documentation อื่นๆที่มีอยู่แล้วในภาษาอังกฤษ เลยสรุปได้ว่าน่าจะเน้นไปที่เนื้อหาสำหรับมือใหม่ เพื่อที่จะเพิ่มจำนวนนักพัฒนาที่สามารถเข้าใจพื้นฐาน แล้วนำไปต่อยอดกันเองนะครับ

จั่วหัวว่าเป็นการศึกษาด้วยบอร์ด ราสเบอร์รี่พาย แต่กว่า จะได้ไปลองบนบอร์ดจริงคงปาเข้าไป สองถึงสามตอนก่อน เพราะอย่างที่บอกตอนต้นว่า ต้องปูพื้นกันก่อนที่จะเริ่มไปเขียนโค้ดไดร์เวอร์จริงๆ หัวข้อที่พูดถึงด้านล่างต่อจากนี้ จะมีความเกี่ยวข้องกับการเขียนลินุกซ์ดีไวซ์ไดร์เวอร์ ทั้งขั้นเริ่มต้นและขั้นสูง ยังไงลองศึกษาไปกันก่อนละกัน

เคอร์เนลสเปซ และ ยูสเซอร์สเปซ (Kernel space & User space)

ความเข้าใจพื้นฐานเรื่องนึงที่เกี่ยวข้องกับลินุกซ์ดีไวซ์ไดร์เวอร์ คือขอบเขตของหน่วยความจำของโปรแกรมฝั่งเคอร์เนลสเปซ และหน่วยความจำของโปรแกรมฝั่งยูสเซอร์สเปซ

userspace_kernel_space_w

เคอร์เนลสเปซ คือบริเวณของหน่วยความจำที่โปรแกรมฝั่งเคอร์เนลสเปซทำงานอยู่ ซึ่งประกอบไปด้วยตำแหน่งหน่วยความจำจริง (Physical memory) และตำแหน่งหน่วยความจำเสมือน (Kernel virtual memory) หน่วยความจำส่วนนี้จะไม่ถูกย้ายลงไปเก็บในดิสก์และไม่สามารถเรียกใช้ได้โดยตรงจากโปรแกรมฝั่งยูสเซอร์สเปซเนื่องจากเหตุผลเรื่องความปลอดภัยของระบบ หน่วยความจำส่วนนี้ครอบคลุมฮาร์ดแวร์ทั้งหมดซึ่งรวมถึงรีจิสเตอร์ต่างๆด้วย

ยูสเซอร์สเปซ คือบริเวณของหน่วยความจำที่โปรแกรมฝั่งยูสเซอร์สเปซทำงานอยู่ ซึ่งเป็นตำแหน่งหน่วยความจำเสมือน (Virtual memory) ซึ่งถูกจัดสรรโดยเคอร์เนล หน่วยความจำส่วนนี้ อาจถูกย้ายไปเก็บใน swap space บนดิสก์ ขึ้นกับระดับความสำคัญที่ระบบปฏิบัติการ ตัดสินใจ

โปรแกรมฝั่งเคอร์เนลสเปซ คือโค้ดที่ทำหน้าที่ติดต่อและควบคุมฮารด์แวร์ต่างๆ รวมถึงจัดการทรัพยากรที่มีในระบบทั้งหมดตามที่โปรแกรมฝั่งยูสเซอร์สเปซร้องขอ

โปรแกรมฝั่งยูสเซอร์สเปซ คือโค้ดที่ทำหน้าติดต่อกับผุ้ใช้ รับผิดชอบประมวลผลสำหรับงานในส่วนที่ยูสเซอร์ต้องการ ส่วนโปรแกรมแอพพลิเคชั่นต่างๆที่ทำงานกับฮาร์ดแวร์นั้น จริงๆแล้วไม่ได้ควบคุมฮาร์ดแวร์นั้นโดยตรง แต่ทำงานโดยผ่านไดร์เวอร์ของอุปกรณ์นั้นอีกทีหนึ่ง

โดยปกติการพัฒนาซอฟแวร์บนลินุกซ์ส่วนใหญ่จะอยู่ในส่วนยูสเซอร์สเปซเป็นหลัก ( C,C++, Python , Java , shell script, php etc. ) ไม่เกี่ยวกับว่าเราเขียนโค้ดด้วยภาษาอะไร ขอบเขตการทำงานก็อยู่ภายใต้ยูสเซอร์สเปซทั้งหมด

เหตุผลที่ต้องแบ่งเมมโมรี่สเปซออกเป็นสองส่วนที่ว่า แอดมินคิดว่ามีสองเหตุผลหลักๆ

ความปลอดภัยของระบบ  เนื่องจากว่า โค้ดที่ทำงานอยู่ในเคอร์เนลสเปซนั้นสามารถเข้าถึงตำแหน่งหน่วยความจำจริง (Physical memory)ได้ทั้งหมด ดังนั้นหากนักพัฒนาเขียนโค้ดไม่รอบคอบ ข้อผิดพลาดนั้นก็สามารถทำให้ระบบล่มได้ รวมถึงความตั้งใจที่จะเข้าไปเอาข้อมูลของผู้ใช้งานคนอื่นที่อยู่ในระบบเดียวกันด้วย ด้วยเหตุนี้การแยกส่วนควบคุมทรัพยากรของระบบ ออกจากโปรแกรมแอพพลิเคชั่นที่อาจจะมาเพิ่มเข้าไปในระบบทีหลัง จึงสามารถเพิ่มระดับความปลอดภัยของข้อมูลและความน่าเชื่อถือของระบบได้ (อย่าลืมว่า ลินุกซ์นั้นสามารถสเกลได้ตั้งแต่ระบบเล็กๆจนไปถึงระบบใหญ่)

ความยืดหยุ่นสำหรับการพัฒนา  การแยกส่วนของการใช้งานหน่วยความจำแบบนี้ จะทำให้การจำกัดขอบเขตของแอพพลิเคชั่นนั้นชัดเจน ทำให้การเขียนโค้ดเพียงครั้งเดียวแล้วสามารถนำไปใช้งานในระบบปฏิบัติการอื่นนอกจากลินุกซ์ทำได้ง่ายมากขึ้น

พื้นฐานการทำงานของระบบแฟ้มอุปกรณ์และลินุกซ์ดีไวซ์ไดร์เวอร์

ระบบปฏิบัติการลินุกซ์เป็นระบบที่ตั้งใจเลียนแบบการทำงานของระบบปฏิบัติการยูนิกซ์ (ต่อไปจะเรียกแค่ลินุกซ์ และ ยูนิกซ์) ดังนั้นแนวคิดหลายๆอย่างก็ได้ถูกนำมาใช้แบบเดียวกัน
แนวคิดเรื่องนึงที่เกี่ยวข้องกับระบบลินุกซ์ดีไวซ์ไดร์เวอร์ ก็คือ “Everything is a file” หมายถึงทุกสิ่งทุกอย่างที่อยู่ในระบบลินุกซ์ จะเข้าถึงได้ผ่านระบบแฟ้ม หรือ file system
การออกแบบระบบลินุกซ์ดีไวซ์ไดร์เวอร์ตามแนวคิดนี้ทำให้ต้องมีไฟล์ที่เป็นตัวแทนของอุปกรณ์นั้นอยู่บนระบบไฟล์ สำหรับให้โปรแกรมฝั่ง user space ติดต่อได้ เราเรียกไฟล์เหล่านี้ว่า device files

Device files หรือ แฟ้มอุปกรณ์ (ในภาษาไทยดูไม่คุ้นเท่าไหร่) โดยปกติจะถูกรวบรวมไว้ภายใต้ /dev/ ของระบบแฟ้มของลินุกซ์ จัดเป็นไฟล์ชนิดพิเศษของระบบลินุกซ์

drwxr-xr-x  8 root root           160 Nov 24 09:02 disk
drwxr-xr-x  2 root root           140 Nov 24 09:02 dri
crw-------  1 root root      247,   0 Nov 24 09:02 drm_dp_aux0
lrwxrwxrwx  1 root root             3 Nov 24 09:02 dvd -> sr0
lrwxrwxrwx  1 root root             3 Nov 24 09:02 dvdrw -> sr0
crw-------  1 root root       10,  61 Nov 24 09:02 ecryptfs
crw-rw----  1 root video      29,   0 Nov 24 09:02 fb0
lrwxrwxrwx  1 root root            13 Nov 24 09:02 fd -> /proc/self/fd
crw-rw-rw-  1 root root        1,   7 Nov 24 09:02 full
crw-rw-rw-  1 root root       10, 229 Nov 24 09:02 fuse
crw-------  1 root root      246,   0 Nov 24 09:02 hidraw0
crw-------  1 root root      246,   1 Nov 24 09:02 hidraw1
crw-------  1 root root      246,   2 Nov 24 09:02 hidraw2
crw-------  1 root root       10, 228 Nov 24 09:02 hpet
drwxr-xr-x  2 root root             0 Nov 24 09:02 hugepages
crw-------  1 root root       10, 183 Nov 24 09:02 hwrng
crw-------  1 root root       89,   0 Nov 24 09:02 i2c-0
crw-------  1 root root       89,   1 Nov 24 09:02 i2c-1
crw-------  1 root root       89,   2 Nov 24 09:02 i2c-2
crw-------  1 root root       89,   3 Nov 24 09:02 i2c-3
crw-------  1 root root       89,   4 Nov 24 09:02 i2c-4
crw-------  1 root root       89,   5 Nov 24 09:02 i2c-5
crw-------  1 root root       89,   6 Nov 24 09:02 i2c-6

กลุ่มที่ขึ้นต้นด้วย b และ c คือ device files แบ่งเป็น block device (b) และ character device (c)

แนวคิดเรื่อง block device และ character device ก็นำมาจากยูนิกซ์เหมือนกัน สองรูปแบบนี้แตกต่างกันคือเรื่องการเข้าข้อมูลแบบระบุตำแหน่งได้ (block device) กับแบบที่ต้องเข้าถึงแบบลำดับ (character device) ในตอนนี้ให้เข้าใจเพียงเท่านี้ก่อน ในรายละเอียดเราจะพูดถึงอีกที ในตอนที่เกี่ยวกับการเขียนไดร์เวอร์ของอุปกรณ์เหล่านี้