สองตอนก่อนหน้าเป็นการพูดถึงสิ่
การอินเตอร์เฟสระหว่างดีไวซ์ไดร์เวอร์และโปรแกรมฝั่งยูสเซอร์สเปซ
เนื่องจากว่าเหตุผลเรื่องความปลอดภัยของระบบลินุกซ์แนวคิดเรื่องการติดต่อกับอุปกรณ์บนระบบจึงต้องทำผ่านระบบแฟ้มซึ่งทำให้การควบคุมสิทธิในการเข้าถึงสามารถใช้รูปแบบเดียวกับไฟล์ทั่วๆไปได้ (permission) ข้อดีของวิธีนี้คือทำให้เข้าใจง่ายแยกหน้าที่ของส่วนต่างๆได้อย่างชัดเจน แต่ก็มีข้อเสียคือทำให้มีโอเวอร์เฮด(เวลา)ของการเข้าถึงของโปรแกรมฝั่งยูสเซอร์สเปซไปยังตัวอุปกรณ์ ซึ่งถ้าเทียบกับการเขียนโปรแกรมติดต่ออุปกรณ์แบบที่ไม่มีระบบปฎิบัติการจะเห็นว่าแบบหลังทำงานได้เร็วกว่า แต่โดยทั่วๆไปเวลาที่เสียไปส่วนนี้อาจไม่สนใจก็ได้หากไม่ใช่งานแบบเรียลไทม์
การทำงานผ่านระบบแฟ้มนี้ทำให้เรานำความรู้เกี่ยวกับการทำงานกับไฟล์ในภาษาซี มาใช้ได้ทันที เนื่องจากเหมือนกันเป๊ะ file operations open(),read(),write() และ close()
เมื่อเราต้องเขียนโปรแกรมที่ต้องทำงานกับอุปกรณ์บนระบบลินุกซ์ เราจำเป็นต้องต้องรู้แน่นอนว่า ดีไวซ์ไฟล์ของอุปกรณ์นั้นคืออะไร (ในที่นี้จะพูดถึงเฉพาะกรณีที่เราต้องเขียนติดต่อกับอุปกรณ์โดยตรง ไม่ผ่านไลบรารี่ เพราะจะเข้าใจได้ง่ายกว่า) จากนั้นจึงเริ่มจากการเปิดไฟล์ขึ้นมาทำงานโดยฟังก์ชั่น open() เพื่อที่จะได้รับ file descriptor มาใช้ในการอ้างอิงในการทำงานภายหลัง
การทำงานของฟังก์ชัน open() จะทำให้เกิด system call ไปยัง kernel เมื่อระบบรับรู้และตรวจสอบได้ว่าเป็นการทำงานกับแฟ้มอุปกรณ์ (device file) ก็จะค้นหาว่าโมดูลใดรับผิดชอบอุปกรณ์นั้นโดยดูจากหมายเลขหลัก(major number) ของแฟ้มอุปกรณ์นั้น และก็จะส่งต่อการทำงานไปยังดีไวซ์ไดร์เวอร์โมดูลนั้นต่อไป
การทำงานส่วนที่เหลือจะขึ้นอยู่กับว่าโปรแกรมฝั่
ตัวอย่างการรีจิสเตอร์ฟังก์ชั่นต่างๆของ nvram driver (linux-3.0.4/drivers/char/generic_nvram.c) ของลินุกซ์เคอร์เนล 3.0.4
131 132 133 134 135 136 137 |
const struct file_operations nvram_fops = { .owner = THIS_MODULE, .llseek = nvram_llseek, .read = read_nvram, .write = write_nvram, .unlocked_ioctl = nvram_unlocked_ioctl, }; |
ตัวอย่างการทำงานของโปรแกรมฝั่งยูสเซอร์สเปซกับลินุกซ์ดีไวซ์ไดร์เวอร์
เพื่อให้เห็นภาพรวมชัดเจนขึ้น เราจะมาลองทำความเข้าใจการทำงานของโปรแกร
เราจะใช้โปรแกรม minicom กับ สาย usb to uart หลังจากเสียบสายเข้าเครื่องคอมข
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 จะถูกเรียกมาทำงาน
1372 1373 1374 1375 1376 1377 1378 1379 |
if (dial_tty == NULL) { if (!dosetup) { while ((dial_tty = get_port(P_PORT)) != NULL && open_term(doinit, 1, 0) < 0) ; if (dial_tty == NULL) exit(1); } } |
(main.c: บรรทัด 288) เมื่อตรวจสอบว่าค่าของตัวแปร dial_tty ไม่ใช่ซ็อกเก็ตไฟล์ ก็จะใช้ฟังก์ชั่น open() ในการเปิดไฟล์ซึ่งเป็น system calls และส่ง file descriptor กลับมา จะเห็นว่าการเปิดดีไวซ์ไฟล์ขึ้น
- O_RDWR: เปิดไฟล์มาเพื่ออ่านเขียน
- O_NDELAY: เปิดไฟล์ในโหมด nonblocking (มีผลเฉพาะ character device)
- O_NOCTTY: ระบุไฟล์ที่เปิดไม่ใช้เป็นเทอร์
มินอลควบคุม
286 287 288 289 290 291 292 293 294 295 296 297 298 |
if (!portfd_is_socket) { #if defined(O_NDELAY) && defined(F_SETFL) portfd = open(dial_tty, O_RDWR|O_NDELAY|O_NOCTTY); if (portfd >= 0) { /* Cancel the O_NDELAY flag. */ n = fcntl(portfd, F_GETFL, 0); fcntl(portfd, F_SETFL, n & ~O_NDELAY); } #else if (portfd < 0) portfd = open(dial_tty, O_RDWR|O_NOCTTY); #endif } |
ปิดท้ายนิดหน่อย
บทต่อๆไปหลังจากนี้เราจะมาลองเขียนโค้ดกันแล้วนะครับ เพราะแอดมินลองหาหลายๆวิธีในการอธิบายแล้ว….สรุปได้ว่าหากได้ลงมือทำหลายๆคนคงเข้าใจได้ดีขึ้น ตอนนี้ก็คงจบเท่านี้ก่อน หวังว่าคงไม่นานมากไปสำหรับตอนต่อไปนะครับ