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

By | March 14, 2017

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

การอินเตอร์เฟสระหว่างดีไวซ์ไดร์เวอร์และโปรแกรมฝั่งยูสเซอร์สเปซ

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

การทำงานผ่านระบบแฟ้มนี้ทำให้เรานำความรู้เกี่ยวกับการทำงานกับไฟล์ในภาษาซี มาใช้ได้ทันที เนื่องจากเหมือนกันเป๊ะ file operations open(),read(),write() และ close()

เมื่อเราต้องเขียนโปรแกรมที่ต้องทำงานกับอุปกรณ์บนระบบลินุกซ์ เราจำเป็นต้องต้องรู้แน่นอนว่า ดีไวซ์ไฟล์ของอุปกรณ์นั้นคืออะไร (ในที่นี้จะพูดถึงเฉพาะกรณีที่เราต้องเขียนติดต่อกับอุปกรณ์โดยตรง ไม่ผ่านไลบรารี่ เพราะจะเข้าใจได้ง่ายกว่า) จากนั้นจึงเริ่มจากการเปิดไฟล์ขึ้นมาทำงานโดยฟังก์ชั่น open() เพื่อที่จะได้รับ file descriptor มาใช้ในการอ้างอิงในการทำงานภายหลัง

การทำงานของฟังก์ชัน open() จะทำให้เกิด system call ไปยัง kernel เมื่อระบบรับรู้และตรวจสอบได้ว่าเป็นการทำงานกับแฟ้มอุปกรณ์ (device file) ก็จะค้นหาว่าโมดูลใดรับผิดชอบอุปกรณ์นั้นโดยดูจากหมายเลขหลัก(major number) ของแฟ้มอุปกรณ์นั้น และก็จะส่งต่อการทำงานไปยังดีไวซ์ไดร์เวอร์โมดูลนั้นต่อไป

การทำงานส่วนที่เหลือจะขึ้นอยู่กับว่าโปรแกรมฝั่งยูสเซอร์สเปซจะทำอย่างไรต่อหลังจากได้รับ file descriptor ไป ซึ่งสามารถทำได้ตาม file operations ที่มี (แต่ไม่)ทั้งหมด เช่น read(),write() เพราะเนื่องจากยังขึ้นอยู่กับว่าดีไวซ์ไดร์เวอร์โมดูลได้ implement ฟังก์ชั่นใดมารองรับบ้าง

ตัวอย่างการรีจิสเตอร์ฟังก์ชั่นต่างๆของ nvram driver (linux-3.0.4/drivers/char/generic_nvram.c) ของลินุกซ์เคอร์เนล 3.0.4

ตัวอย่างการทำงานของโปรแกรมฝั่งยูสเซอร์สเปซกับลินุกซ์ดีไวซ์ไดร์เวอร์

เพื่อให้เห็นภาพรวมชัดเจนขึ้น เราจะมาลองทำความเข้าใจการทำงานของโปรแกรม minicom ซึ่งเป็นโปรแกรมจัดการเทอร์มินอลที่หลายคนอาจเคยใช้งานมาบ้างแล้ว ซึ่งเป็นตัวอย่างที่ดีเพราะมีทั้งส่วนติดต่อผู้ใช้และส่วนที่ติดต่อกับอุปกรณ์พอร์ตอนุกรม(ผ่านดีไวซ์ไดร์เวอร์) เราจะเห็นตัวอย่างของ file operations read(), write() และ ioctl().

เราจะใช้โปรแกรม minicom กับ สาย usb to uart หลังจากเสียบสายเข้าเครื่องคอมของเรา ตัวดีไวซ์ไฟล์ก็จะถูกสร้างภายใต้ห้อง /dev/ อัตโนมัติ

crw-rw-r-- 1 root dialout 188, 0 Dec 20 08:27 /dev/ttyUSB0

จากตารางด้านบน จะสังเกตุได้ว่าค่า major number จะเป็น 188, minor number จะเป็น 0 โดยมี owner เป็น root และ group เป็น dialout แอดมินยังไม่มีตัวอย่างที่อธิบายการนำค่าเหล่านี้ไปใช้ได้แบบชัดเจนเนื่องจาก major number และ minor number จะถูกใช้ภายในส่วนของ kernel space ในบทนี้จึงขอข้ามการอธิบายเรื่องนี้ไปก่อนนะครับ

เราสามารถรันโปรแกรม minicom จากคำสั่งด้านล่างนี้

minicom -o -b 115200 -D /dev/ttyUSB0

โดยแต่ละออฟชั่นมีความหมายดังนี้:

  • กำหนดให้เริ่มการทำงานโดยไม่ตั้งค่าเริ่มต้นให้กับโมเดม(-o)
  • กำหนดความเร็วในการสื่อสารที่ 115200 (-b 115200)
  • ระบุอุปกรณ์ที่ทำงานคือ /dev/ttyUSB0 (-D /dev/ttyUSB0)

หลังจากที่เรากดปุ่ม Enter เพื่อสั่งให้โปรแกรมทำงาน ลำดับขั้นตอนการทำงานด้านล่างก็จะเริ่มขึ้น

** ตัวซอร์ซโค้ดของโปรแกรม minicom ที่ใช้ในการดีบักนี้มาจาก version minicom-2.7 นะครับ

(minicom.c: บรรทัด 1008 – 1293) เมื่อเริ่มการทำงาน ฟังก์ชั่น main() ที่อยู่ในไฟล์ minicom.c จะอ่านค่าอาร์กิวเมนต์ต่างๆ ที่เราป้อนเข้าไป ตัวแปร cmdline_baudrate จะเก็บค่า “115200” และ ตัวแปร cmdline_device จะเก็บค่า “/dev/ttyUSB0” เอาไว้

(minicom.c: บรรทัด 1374) ค่าในตัวแปร cmdline_device จะถูกส่งต่อไปให้ตัวแปร dial_tty แล้วฟังก์ชั่น openterm() ในไฟล์ main.c จะถูกเรียกมาทำงาน

(main.c: บรรทัด 288) เมื่อตรวจสอบว่าค่าของตัวแปร dial_tty ไม่ใช่ซ็อกเก็ตไฟล์ ก็จะใช้ฟังก์ชั่น open() ในการเปิดไฟล์ซึ่งเป็น system calls และส่ง file descriptor กลับมา จะเห็นว่าการเปิดดีไวซ์ไฟล์ขึ้นมาทำงานเป็นวิธีเช่นเดียวกับการทำงานกับไฟล์ปกติ ส่วนแฟล็กสามตัวที่ระบุเข้ามา มีผลดังนี้

  • O_RDWR: เปิดไฟล์มาเพื่ออ่านเขียน
  • O_NDELAY: เปิดไฟล์ในโหมด nonblocking (มีผลเฉพาะ character device)
  • O_NOCTTY: ระบุไฟล์ที่เปิดไม่ใช้เป็นเทอร์มินอลควบคุม

ปิดท้ายนิดหน่อย

บทต่อๆไปหลังจากนี้เราจะมาลองเขียนโค้ดกันแล้วนะครับ เพราะแอดมินลองหาหลายๆวิธีในการอธิบายแล้ว….สรุปได้ว่าหากได้ลงมือทำหลายๆคนคงเข้าใจได้ดีขึ้น ตอนนี้ก็คงจบเท่านี้ก่อน หวังว่าคงไม่นานมากไปสำหรับตอนต่อไปนะครับ