--- title: Driver (驅動) - 從零開始的開源地下城 tags: Linux, Linux讀書會, Kernel, 從零開始的開源地下城, COMBO-tw description: 介紹與Linux Kernel相關基本知識 lang: zh-Hant GA: G-2QY5YFX2BV --- # Driver (驅動) ###### tags: `Linux` ## 目錄 [TOC] ## Topic Block Device Character Device Ref: [LDD3](https://lwn.net/Kernel/LDD3/) ## Introduction 在我們的裝置上,不論是電腦、手機甚至是網通設備這樣無法與人直接互動的設備等,皆需要 Driver 的存在,常見的裝置有"藍芽"、"網路卡"或是"鍵盤滑鼠"...等,然而這些裝置如果沒有像 Driver 這樣偏向硬體底層的軟體,那使用者將會需要耗費需多資源來進行程式的開發。 ![](https://hackmd.io/_uploads/rycTWoWU3.png) 上圖為 Linux Kernel 的拆解分視圖,在一部嵌入式裝置上會提供許多功能,例如 行程管理 (Process management)、記憶體管理 (Memory management)、檔案系統格式驅動 (Filesystems)、裝置設備控制 (Device control)及網路驅動控制 (Networking)等。大致上分為以下幾種**Block devices**、**Character devices**,在後面將會詳細介紹這幾種驅動型態。 在本章節將介紹如何建立一個驅動程式來讓 User Space 程式來進行使用,並且完成 Block Device 及 Character Device 兩種類型的實作,。 ## 驅動程式類型介紹 ### Block devices Block devices 可透過 **/dev** 中的 Filesystem nodes 來 Accessed。一個 Block 只能處理一到多個完整 Block 的 I/O 操作,大小為 512 bytes (或是更大的2的次方)。Linux 可以讓 User Space 讀取 Block device 如同操作File 一樣,而不用一次讀一整個 Block,在 User Space 中使用與 Char 基本上無差別。有差別的是在 Kernel & Driver 這一層完全不同。 ### Character devices Character devices 可以被當作 **A stream of bytes** 來被 Accessed,就像 File 一樣。因此 Char dirver 就要負責至少 open, close, read, and write 的 system calls 操作,有的還有提供 ioctl 的操作方式。常見的例子有 Text Console (/dev/console) and the Serial Ports (/dev/ttyS0),都是 Streaming 結構。Char devices 由 Filesystem nodes 來 accessed, such as /dev/tty1 and /dev/lp0。但跟一般 File 不同的是,一般 File 可以把指標往前往後來操作檔案,但是多數 Char device 只是 Data Channels,只能依序 Access。當然也有例外,而該例外多出現在 Frame Grabbers,就會像 Data Areas 一樣,可以在之中隨意前後操作,多用 mmap or lseek 來配合使用。 ### Block 與 Character 的差異 Block devices 是以固定大小長度來傳送轉移資料,而 Character devices 是以不定長度的字元傳送資料,而其操作方式在 User Spcae 則大同小異。 ## 實際動手做做看吧 ### Block devices 那麼首先就是來建立一個空的 Block devices 吧,而我們要如何建立一支 Driver 呢? 那就必須要有他的主程式,在此我們命名為`my_block.c`,而內容如下: ```clike= #include <linux/init.h> #include <linux/module.h> #include <linux/blkdev.h> MODULE_LICENSE("Dual BSD/GPL"); #define MY_BLOCK_MAJOR 240 #define MY_BLKDEV_NAME "my_block" static __init int my_block_init(void) { int status; status = register_blkdev(MY_BLOCK_MAJOR, MY_BLKDEV_NAME); if (status < 0) { printk(KERN_ERR "unable to register mybdev block device\n"); return -EBUSY; } printk(KERN_INFO "Hello kernel\n"); return 0; } static void __exit my_block_exit(void) { printk(KERN_INFO "Goodbye\n"); unregister_blkdev(MY_BLOCK_MAJOR, MY_BLKDEV_NAME); } module_init(my_block_init); module_exit(my_block_exit); ``` 而每一隻 Driver 都有共通的函式,`init`及`exit`,分別式掛載時用與卸載時用,然而 Block driver 需要向 Kernel 註冊自己,需要使用到 `<linux/blkdev.h>` 中的 `register_blkdev` 及 `unregister_blkdev` 來進行註冊與註銷的動作。 而每個 Block Device 都必須有個 Major Number ,該編號在 Kernel 中可使用的範圍定義於 `linux/kdev_t.h` 當中。掛載的方式為 `insmod <DRIVER_NAME.ko>` ,掛在上去後可用 `sed -n '/^Block/, /^$/ { /^$/ !p }' /proc/devices` 來查看裝置是否存在。 相關的 Source Code 請參考 Github [04-driver](https://github.com/combo-tw/LinuxBookClub/tree/master/chapter/04-driver) ### Character devices 在前一段中,我們說明了 Block Devices Driver 的建立以及 Driver 基本所需的函式,而在此將說明如何建立一個簡單的 Character Devices Driver,在此我們命名為`my_char.c`,而內容如下: ```clike= #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <asm/ioctl.h> MODULE_LICENSE("Dual BSD/GPL"); #define MY_CDEB_MAJOR 42 #define MY_CDEV_NAME "my_char" #define MY_MAX_MINORS 5 struct my_device_data { struct cdev cdev; }; struct my_device_data devs[MY_MAX_MINORS]; const int arr[5]; static int my_open(struct inode *inode, struct file *file) { return 0; } static int my_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset) { return 0; } static int my_write(struct file *file, const char __user *user_buffer, size_t size, loff_t * offset) { return 0; } static int my_release(struct inode *inode , struct file *filp) { return 0; } static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { return 0; } const struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .read = my_read, .write = my_write, .release = my_release, .unlocked_ioctl = my_ioctl }; static __init int my_char_init(void) { int i, err; err = register_chrdev_region(MKDEV(MY_CDEB_MAJOR, 0), MY_MAX_MINORS, MY_CDEV_NAME); if (err != 0) { /* report error */ return err; } for(i = 0; i < MY_MAX_MINORS; i++) { /* initialize devs[i] fields */ cdev_init(&devs[i].cdev, &my_fops); cdev_add(&devs[i].cdev, MKDEV(MY_CDEB_MAJOR, i), 1); } printk(KERN_INFO "Hello kernel\n"); return 0; } static void __exit my_char_exit(void) { int i; printk(KERN_INFO "Goodbye\n"); for(i = 0; i < MY_MAX_MINORS; i++) { /* release devs[i] fields */ cdev_del(&devs[i].cdev); } unregister_chrdev_region(MKDEV(MY_CDEB_MAJOR, 0), MY_MAX_MINORS); } module_init(my_char_init); module_exit(my_char_exit); ``` 為了方便說明以下簡稱`Block`與`Char`,而與 Block 不同的是, Char 具有 read, write 等功能,使得使用者可以透過 Driver 來對 Devices 進行 Register 操作,進而做到 User Space 使用通用 Interface 即可使用多種多樣化的 Devices 。 可透過 `sed -n '/^Character/, /^$/ { /^$/ !p }' /proc/devices` 來查看裝置是否存在。 相關的 Source Code 請參考 Github [04-driver](https://github.com/combo-tw/LinuxBookClub/tree/master/chapter/04-driver) 在此我們將特別針對兩個常用函式進行說明,首先是 `read()`: ```clike= static int my_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset) { return 0; } ``` 此函數是使用者透過開啟 `file descriptor` 或是稱 `fd` 時,進行 read 操作時會調用之函式,而第一個引數 `sturct file *file` 為該 `fd` 資訊結構體,而第二個引數 `const char __user *user_buffer` 為該次 read 操作時可使用的 User Space 變數指標,第三個引數 `size_t size` 為欲讀取的資料筆數,最後一個引數 `loff_t *offset` 為該 `fd` 對於基準點的偏移量。 ![](https://hackmd.io/_uploads/HkuAbjWU3.png) ```clike= static int my_write(struct file *file, const char __user *user_buffer, size_t size, loff_t * offset) { return 0; } ``` 此函數是使用者透過開啟 `file descriptor` 或是稱 `fd` 時,進行 write 操作時會調用之函式,而第一個引數 `sturct file *file` 為該 `fd` 資訊結構體,而第二個引數 `const char __user *user_buffer` 為該次 write 操作時可使用的 User Space 變數指標,第三個引數 `size_t size` 為欲寫入的資料筆數,最後一個引數 `loff_t *offset` 為該 `fd` 對於基準點的偏移量。 ![](https://hackmd.io/_uploads/H1ACWob82.png) ## 本章節練習與反思 * 請依照 Github 所提供的 Source Code 為範本,設計一個 Character Driver ,讓使用者可以透過 `read()` 及 `write()` 來操作 Driver 內的 `arr` 變數,也就是,每開啟一個 `fd` 就可以操作對應的 `arr` 變數 address,舉例來說,第一個 fd 可以操作 `arr[0]`,第二個 fd 可以操作 `arr[1]` 以此類推。 > 測試程式位於[此](https://github.com/combo-tw/LinuxBookClub/blob/master/chapter/04-driver/test/io_ut.c) > 測試方法為當 QEMU 啟動後,進入 `/home/pi/test` 資料夾,並執行 `make check` 即可 >> Hints: 不可以讓 fd 操作不屬於該 fd 的 Register。 * 思考為什麼 Driver 要設計成這個樣子,需要有註冊、註銷、ioctl、read 及 write 等函式架構?並紀錄於筆記當中。 * 還有試著去了解,當我們使用 `read` 及 `write` 等 System Call 時,它的程式流程,並紀錄於筆記當中。 ## 參考資料 * [LDD3](https://lwn.net/Kernel/LDD3/) * [Linux Device Driver 3 心得整理 (1)](https://medium.com/@wbobw951/linux-device-driver-3-%E5%BF%83%E5%BE%97%E6%95%B4%E7%90%86-ceb91196fa3f) * [[linux]Block Device Driver和 Character Device Driver](https://pcnews.pixnet.net/blog/post/30896162) * [一個簡單的 Linux Kernel Module](https://wwssllabcd.github.io/blog/2012/11/13/how-to-make-linux-module/) * [如何查看 Char and Block Devices](https://askubuntu.com/questions/1068747/how-to-display-list-of-character-devices-and-block-devices-separately) * [Block Device Drivers](https://linux-kernel-labs.github.io/refs/heads/master/labs/block_device_drivers.html) * [Character device drivers](https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html)