2021 LDD3 第3章 === contribute by `Liao712148` [Liao712148](https://github.com/Liao712148/2021ldd) ###### tags: `Linux Device Drivers`, `Linux ` 寫驅動程式的第一步,是==定義你的驅動程式對user-space的程式提供那些功能==。 #### 名詞解釋 * 裝置編號 * 主編號:代表裝置(指scull從kernel配置而來的記憶體)所配合的驅動程式。 可以透過由<linux/kdev_t.h>當中的macro `MAJOR(det_t dev);`來得到。 * 次編號:代表你的驅動程式所實作的裝置,驅動程式可以直接從核心取得一個指向目標裝置的指標。 可以透過由<linux/kdev_t.h>當中的macro `MINOR(det_t dev);`來得到。 * 裝置編號可以利用 `MKDEV(int major, int minor)`來得到。 #### 向kernel索取裝置編號 `int alloc_chrdev_region(dev_t *dev, unsigned int firstmintor, unsigned int count, char *name);` 透過`dev`取得配置範圍的第一個裝置編號。 #### /proc/devices 和 /dev的關係 `/proc/devices/`中的裝置是通過insmod載入到核心的,它可產生一個major供mknod作為 引數。 `/dev/* `是通過mknod加上去的,格式:mknod device1 c/b major minor 如:mknod /dev/ttyS0 c 4 64,使用者通過此裝置名來訪問你的驅動。 #### 透過script 來載入scull module 透過scull_load來載入module ```shell= #!/bin/sh module="scull" device="scull" mode="664" # Group: since distributions do it differently, look for wheel or use staff if grep -q '^staff:' /etc/group; then group="staff" else group="wheel" fi # invoke insmod with all arguments we got # and use a pathname, as insmod doesn't look in . by default insmod ./$module.ko $* || exit 1 # retrieve major number major=$(awk "\$2==\"$module\" {print \$1}" /proc/devices) # Remove stale nodes and replace them, then give gid and perms # Usually the script is shorter, it's scull that has several devices in it. rm -f /dev/${device}[0-3] mknod /dev/${device}0 c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2 mknod /dev/${device}3 c $major 3 ln -sf ${device}0 /dev/${device} chgrp $group /dev/${device}[0-3] chmod $mode /dev/${device}[0-3] ``` `第5行`:664表示只有owner,group才有寫入的權限,others只有讀取的權限。[檔案權限](https://shian420.pixnet.net/blog/post/344938711-%5Blinux%5D-chmod-%E6%AA%94%E6%A1%88%E6%AC%8A%E9%99%90%E5%A4%A7%E7%B5%B1%E6%95%B4!) `第15行`:載入module`insomd ./$module.ko`,因為insmod不會尋找當時工作目錄所以模組檔名之前要加上`./`,並利用`$*`傳入所有命令列的所有引數。最後有一個`||`的操作,如果載入不成功就會exit。 `第18行`:[awk類似grep](https://ithelp.ithome.com.tw/articles/10136126), /proc/devices的第一欄($1)事裝置的主編號。第二攔($2)是裝置名稱。 `第23行`:移除前次裝載所留下來的節點。 `第7~11行`:設定節點的擁有者與群組,有些系統使用wheel群組,但有些系統使用staff群組,所以需要用`if grep -q '^staff:' /etc/group`來判斷。其中`grep -q '^staff:`的`^staff:`代表只搜尋行首的字元,staff:是要搜尋的字元,並且是要在該行的行首。 `第29行`:利用[chgrp](https://www.itread01.com/content/1523008588.html),更改`/dev/${device}[0-3]`的屬主,改成`$group`。讓`$group`的成員也有權寫入scull裝置。 `第30行`:利用[chmod](https://www.itread01.com/content/1523008588.html),更改`/dev/${device}[0-3]`的九個屬性,改成`$mode`代表只有owner,group才有寫入的權限,others只有讀取的權限。 [如號撰寫shell script](https://blog.techbridge.cc/2019/11/15/linux-shell-script-tutorial/) #### 取得裝置編號 ==當驅動程式設定字元裝置時,要做的第一件事事先取得一個或多個裝置編號== ```cpp= int scull_init_module(void) { int result, i; dev_t dev = 0; /* * Get a range of minor numbers to work with, asking for a dynamic * major unless directed otherwise at load time. */ /*不管是靜態/動態allocate裝置編號,只要一allocate完後就可以在/proc/devices中找到scull所被分配到的主編號*/ if (scull_major) { dev = MKDEV(scull_major, scull_minor); /*使用靜態分配,所以必須事先知道major*/ result = register_chrdev_region(dev, scull_nr_devs, "scull"); } else { /*使用動態分配*/ result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); scull_major = MAJOR(dev); } if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; } /*...*/ } ``` #### 重要的資料結構 驅動程式中的基本操作,多半涉及kernel的3種重要的資料結構,分別是`file_operations`,`file`,`inode`。 ##### file_operatoins 定義在`<linux/fs.h>`,是一個操作檔案方法的集合。成員多是function pointer,所以file_operations這個資料結構有點像是工具箱,每個工頭(file)的工具相當中的工具部會完全相同,透過這些工具箱的內容,我們就可以知道這個工頭會哪一些東西。對於每個已經開啟的檔案(裝置)會以file結構表示,核心會各自賦予一組專屬的作業函式,也就是==file結構中的f_op會指向該檔案的作業函式的集合file_operations結構==。因此在對該檔案(裝置)呼叫system call時,會有專屬於該檔案(裝置)作業函式來對接。 ##### file 定義在`<linux/fs.h>`的struct file,==file與user space當中的FILE指標毫無關西==,FILE是定義於C函式庫,不可能出現在kernel當中的程式碼中,而file結構也不會出現在user space的應用程式裡。 file結構代表已開啟的檔案(open file),這不是驅動程式的專利,對於系統上每個已經開啟的檔案,在kernel space都有一個對應的struct file。每一個file結構都是kernel在收到open()系統呼叫時自動成立的,然後在最後一次close()系統呼叫返回。也就是沒有任何process持有file結構的指標時,kernel才會釋放file結構。 file struct成員當中最重要的就是 `struct file_operations *f_op`,他指向了上述的file_operations。 ##### inode Kernel內部使用inode結構來代表檔案,它不同於file結構,file結構是代表以開啟的檔案的FD(file descriptor)。如果同一個檔案配重複開啟多次,這樣的話kernel會有多個代表FD的file結構,但是他們==都指向同一個inode結構==。indoe結構中我們只在意 * dev_t i_rdev 對於代表裝置檔的inodes,此欄位含有實際的裝置編號。 ==盡量使用以下macro,避免直接操作i_rdev== *`unsigned int iminor(struct inode *inode);` *`unsigned int imajor(struct inode *inode);` * struct cdev *i_cdev kernel內部用來表示字元裝置的結構,對於代表字元裝置的inode,此欄位含有一個指向該結構的指標。 #### 註冊字元裝置 ##### struct cdev 是裝置與核心之間的介面。由於我們希望將struct cdev(linux/cdev.h)這個結構 ![](https://i.imgur.com/FOEW25z.png) 嵌入到我們所定義的結構(scull_dev)之中, ![](https://i.imgur.com/eVxm9TN.png) 因此要使用`cdev_add`初始化cdev的結構,並將cdev結構中的`*ops`指向我們所定義的file_operations(scull_fops)。 --- #### 檔案構想 ##### step1: 定義檔案的內容 ```cpp= struct scullc_dev { void **data; struct scullc_dev *next; /* next listitem */ int quantum; /* the current allocation size */ int qset; /* the current array size */ size_t size; /* 32-bit will suffice */ struct mutex lock; /* Mutual exclusion */ struct cdev cdev; }; ``` ##### step2: 定義檔案支援的函式 ```cpp= struct file_operations scullc_fops = { .owner = THIS_MODULE, .llseek = scullc_llseek, .read = scullc_read, .write = scullc_write, .open = scullc_open, .release = scullc_release, }; ``` ##### step3: 定義函式 * scullc_read, scullc_write在使用裝置時,==皆需要取得lock==。 * scullc_open的主要功能就是透過inode中的i_cdev,取得該裝置的scullc_dev,並將scullc_dev存入filp->private_data。 ##### step4: 初使化檔案的內容 ```cpp= int scullc_init(void) { int result, i; dev_t dev = MKDEV(scullc_major, 0); /* * Register your major, and accept a dynamic number. */ if (scullc_major) /*靜態分配裝置編號*/ result = register_chrdev_region(dev, scullc_devs, "scullc"); /* * scullc是決定/peoc/devices中獲得此編號範圍的裝置之名稱 * scullc_devs是你想要申請的連續裝置編號的總數 * */ else { /*動態配置裝置編號*/ result = alloc_chrdev_region(&dev, 0, scullc_devs, "scullc"); scullc_major = MAJOR(dev); } if (result < 0) return result; /*scullc_devices指向由scullc_devs個scullc_dev所組成的陣列*/ scullc_devices = kmalloc(scullc_devs*sizeof (struct scullc_dev), GFP_KERNEL); if (!scullc_devices) { result = -ENOMEM; goto fail_malloc; } memset(scullc_devices, 0, scullc_devs*sizeof (struct scullc_dev)); /*初使化scullc_dev的成員*/ for (i = 0; i < scullc_devs; i++) { scullc_devices[i].quantum = scullc_quantum; scullc_devices[i].qset = scullc_qset; mutex_init (&scullc_devices[i].lock); scullc_setup_cdev(scullc_devices + i, i); /*將由linux/cdev.h.h中定義的cdev嵌入scullc_dev中*/ } scullc_cache = kmem_cache_create("scullc", scullc_quantum, 0, SLAB_HWCACHE_ALIGN, NULL); /* no ctor/dtor */ if (!scullc_cache) { scullc_cleanup(); return -ENOMEM; } return 0; /* succeed */ fail_malloc: unregister_chrdev_region(dev, scullc_devs); return result; } ``` [Kmalloc VS kmem_cache_alloc](https://titanwolf.org/Network/Articles/Article?AID=08762a07-f3e2-4f50-bf30-59511e100efa#gsc.tab=0) [Difference between kmalloc and kmem_cache_alloc](https://stackoverflow.com/questions/22370102/difference-between-kmalloc-and-kmem-cache-alloc) ##### step5: 將由linux/cdev.h.h中定義的cdev嵌入scullc_dev中 補充: <linux/cdev.h> ```cpp= struct cdev { ... struct module *owner; const struct file_operations *ops; ... } ``` ```cpp= static void scullc_setup_cdev(struct scullc_dev *dev, int index) { int err, devno = MKDEV(scullc_major, index); cdev_init(&dev->cdev, &scullc_fops);//將cdev中的ops成員指向scullc_fops,這樣就可以調用此檔案支援的函式 dev->cdev.owner = THIS_MODULE; err = cdev_add (&dev->cdev, devno, 1);//準備好cdev結構之後,以cdev_add()將它加入核心,也就是註冊字元裝置。 /* Fail gracefully if need be */ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); } ``` ==當cdev_add()返回時,表示裝置已經生效了,核心隨時都有可能呼叫你的作業方法==。因此,除非驅動程式已經完全準備妥當,而且目標裝置也準備好開始運作,否則不應該呼叫cdev_add()。 ##### step6: 透過mknod將裝置名稱加入 /dev 利用靜態/動態分配裝置編號後,使用mknod將裝置名稱加入/dev中,使用者可以透過裝置名稱來訪問使用裝置。 ```cpp= /*取得利用靜態/動態分配裝置編號後,已經分配到的主編號*/ major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"` /*major後面的0,1,2,3皆為次編號*/ mknod /dev/${device}0 c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2 mknod /dev/${device}3 c $major 3 ln -sf ${device}0 /dev/${device} # give appropriate group/permissions chgrp $group /dev/${device}[0-3] chmod $mode /dev/${device}[0-3] ``` --- #### 載入scull模組 此時的模組 ![](https://i.imgur.com/UlxGleY.png) /proc/devices 的內容 ![](https://i.imgur.com/RDunuaB.png) /dve 的內容 ![](https://i.imgur.com/hAtA5cT.png) #### 測試scullc模組 example.c ```cpp= #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <linux/fcntl.h> int main() { int fd; char buf[]="this is a example for character devices driver by Liao712148!";//寫入scullc0裝置的內容 char buf_read[4096];//scullc0裝置的內容讀入到該buf中 if((fd=open("/dev/scullc0",O_RDWR))==-1)//使用者透過/dev中的裝置名稱(scullc0)開啟scullc0裝置 printf("open scullc0 WRONG!\n"); else printf("open scullc0 SUCCESS!\n"); printf("buf is %s\n",buf); write(fd,buf,sizeof(buf));//把buf中的內容寫入scullc0裝置 lseek(fd,0,SEEK_SET);//把檔案指標重新定位到檔案開始的位置 read(fd,buf_read,sizeof(buf));//把scullc0裝置中的內容讀入到buf_read中 printf("buf_read is %s\n",buf_read); return 0; } ``` 輸出結果: ![](https://i.imgur.com/Nm7n5kA.png) #### 退出scullc模組 ![](https://i.imgur.com/vM2sDQX.png)