昨天我們對linux內核的子系統進行簡單的認識,今天我們正式進入驅動的開發,我們今后的學習為了避免大家沒有硬件的缺陷,我們都會以虛擬的設備為例進行學習,所以大家不必害怕沒有硬件的問題。
今天我們會分析到以下內容:
1. 字符設備驅動基礎
2. 簡單字符設備驅動實現
3. 驅動測試
1. 字符設備描述結構
在linux2.6內核中,使用cdev結構體描述一個字符設備,其定義如下:
1 struct cdev { 2 struct kobject kobj;/*基於kobject*/ 3 struct module *owner; /*所屬模塊*/ 4 const struct file_operations *ops; /*設備文件操作函數集*/ 5 struct list_head list; 6 dev_t dev; /*設備號*/ 7 unsigned int count; /*該種類型設備數目*/ 8 };
上面結構中需要我們進行初始化的有ops和dev,下面我們會對這兩個成員進行分析。
注:kobject結構是驅動中很重要的一個結構,由於其復雜性,我們現在不進行介紹,后面會詳細介紹。
2. 設備號
1. 何為設備號:cdev結構體中dev成員定義了設備號,而dev_t則為U32類型的也就是32位,其中12位為主設備號,20位為次設備號。我們執行ls –l /dev/可看到下圖,其中左邊紅框為主設備號,右邊為次設備號
2. 何為主設備號:用來對應該設備為何種類型設備。(比如串口我們用一個數字識別,而串口有好幾個)
3. 何為次設備號:用來對應同一類型設備下的具體設備。(用次設備號來具體區分是哪個串口)
4. 設備號相關操作:
1. 通過主設備號和次設備號獲取dev:dev = MKDEV(主,次);
2. 通過dev獲取主設備號:主 = MAJOR(dev);
3. 通過dev獲取次設備號:dev = MINOR(dev);
5. 設備號分配:設備號的分配有兩種方式,一種是靜態的,另一種是動態的,下面一一分析
1. 靜態分配:也就是程序員自己指定設備號,通過register_chrdev_region();函數向內核申請,可能會導致和內核已有的沖突,從而失敗。
2. 動態分配:通過 alloc_chrdev_region(); 函數向內核申請設備號。
3. 釋放設備號:通過 unregister_chrdev_region(); 釋放申請到的設備號。
3. file_operations操作函數集
file_operations結構體中的成員函數在我們驅動開發過程中極為重要,其中的內容相當龐大,下面我們看看其定義:

1 struct file_operations { 2 struct module *owner;/*擁有該結構的模塊的指針,一般為THIS_MODULES*/ 3 loff_t (*llseek) (struct file *, loff_t, int); /*用來修改當前文件的讀寫指針*/ 4 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);/*從設備讀取數據*/ 5 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);/*向設備發送數據*/ 6 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一個異步的讀取操作*/ 7 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一個異步的寫入操作*/ 8 int (*readdir) (struct file *, void *, filldir_t); /*只用於讀取目錄,對於設備文件該字段為NULL*/ 9 unsigned int (*poll) (struct file *, struct poll_table_struct *);/*輪詢函數,判斷目前是否可以進行非阻塞的讀取或寫入*/ 10 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 不用BLK的文件系統,將使用此函數代替ioctl*/ 11 long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 代替ioctl*/ 12 int (*mmap) (struct file *, struct vm_area_struct *);/*用於請求將設備內存映射到進程地址空間*/ 13 int (*open) (struct inode *, struct file *);/*打開*/ 14 int (*flush) (struct file *, fl_owner_t id); /*在進程關閉它的設備文件描述符的拷貝時調用; 它應當執行(並且等待)設備的任何未完成的操作. */ 15 int (*release) (struct inode *, struct file *);/*關閉*/ 16 int (*fsync) (struct file *, int datasync); /*刷新待處理數據*/ 17 int (*aio_fsync) (struct kiocb *, int datasync); /*異步fsync*/ 18 int (*fasync) (int, struct file *, int); /*通知設備FASYNC標志發生變化*/ 19 int (*lock) (struct file *, int, struct file_lock *);/* 實現文件加鎖*/ 20 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*通常為NULL*/ 21 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*在當前的進程地址空間找的一個未映射的內存段*/ 22 int (*check_flags)(int); /*法允許模塊檢查傳遞給 fnctl(F_SETFL...) 調用的標志*/ 23 int (*flock) (struct file *, int, struct file_lock *);/**/ 24 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /*由VFS調用,將管道數據粘貼到文件*/ 25 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /*由VFS調用,將文件數據粘貼到管道*/ 26 int (*setlease)(struct file *, long, struct file_lock **);/**/ 27 long (*fallocate)(struct file *file, int mode, loff_t offset, 28 loff_t len); /**/ 29 };
上面結構體中的函數指針所指向的函數,在我們在進行open、write、read等系統調用的時候最終會被調用到,所以我們的驅動中想為應用層實現那種調用就要在此實現。
4. 字符設備驅動初始化
我們通過上面的分析對設備號和操作函數集有了一定的了解下面我們來看字符設備驅動初始化,其主要步驟如下。
1. 分配cdev結構:有靜態(直接定義)動態(cdev_alloc();)兩種方式
2. 初始化cdev結構:使用 cdev_init(struct cdev *cdev, const struct file_operations *fops) 初始化
3. 驅動注冊:使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count)//count為該種類型的設備個數注冊
4. 硬件初始化:閱讀芯片手冊進行硬件設備的初始化
5. 完成操作函數集:實現要用的操作(設備方法)
6. 驅動注銷:使用 void cdev_del(struct cdev *p) 注銷
5. 字符設備驅動模型及調用關系
下面我通過一張圖將字符設備的驅動結構、以及字符設備驅動與用戶空間的調用關系進行展示:
6. 遺漏知識
我們內核空間和用戶空間的數據交互要用到下面兩個函數:
1 copy_from_user();//從用戶空間讀 2 copy_to_user();//寫入用戶空間
l 簡單字符設備驅動實現
經過上面的分析我們對字符設備有一定了解,下面我們來完成一個最簡單的字符設備驅動。我只展示最主要的代碼,整個項目工程在https://github.com/wrjvszq/myblongs.git歡迎大家關注。
1. 字符設備驅動編寫
因為驅動本身就是一個內核模塊,下面的字符設備驅動只實現了部分方法,在后面的博客中我們會基於此驅動慢慢修改,希望大家掌握。

1 #include<linux/module.h> 2 #include<linux/init.h> 3 #include<linux/cdev.h> 4 #include<linux/fs.h> 5 #include<asm/uaccess.h> 6 7 #define MEM_SIZE 1024 8 9 MODULE_LICENSE("GPL"); 10 11 struct mem_dev{ 12 struct cdev cdev; 13 int mem[MEM_SIZE];//全局內存4k 14 dev_t devno; 15 }; 16 17 struct mem_dev my_dev; 18 19 /*打開設備*/ 20 int mem_open(struct inode *inode, struct file *filp){ 21 int num = MINOR(inode->i_rdev);/*獲取次設備號*/ 22 23 if(num == 0){/*判斷為那個設備*/ 24 filp -> private_data = my_dev.mem;/*將設備結構體指針復制給文件私有數據指針*/ 25 } 26 return 0; 27 } 28 /*文件關閉函數*/ 29 int mem_release(struct inode *inode, struct file *filp){ 30 return 0; 31 } 32 33 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){ 34 int * pbase = filp -> private_data;/*獲取數據地址*/ 35 unsigned long p = *ppos;/*讀的偏移*/ 36 unsigned int count = size;/*讀數據的大小*/ 37 int ret = 0; 38 39 if(p >= MEM_SIZE)/*合法性判斷*/ 40 return 0; 41 if(count > MEM_SIZE - p)/*讀取大小修正*/ 42 count = MEM_SIZE - p; 43 44 if(copy_to_user(buf,pbase + p,size)){ 45 ret = - EFAULT; 46 }else{ 47 *ppos += count; 48 ret = count; 49 } 50 51 return ret; 52 } 53 54 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){ 55 unsigned long p = *ppos; 56 unsigned int count = size; 57 int ret = 0; 58 int *pbase = filp -> private_data; 59 60 if(p >= MEM_SIZE) 61 return 0; 62 if(count > MEM_SIZE - p) 63 count = MEM_SIZE - p; 64 65 if(copy_from_user(pbase + p,buf,count)){ 66 ret = - EFAULT; 67 }else{ 68 *ppos += count; 69 ret = count; 70 } 71 return ret; 72 } 73 74 /*seek文件定位函數*/ 75 static loff_t mem_llseek(struct file *filp, loff_t offset, int whence){ 76 77 loff_t newpos; 78 79 switch(whence) { 80 case SEEK_SET:/*從文件頭開始定位*/ 81 newpos = offset; 82 break; 83 case SEEK_CUR:/*從當前位置開始定位*/ 84 newpos = filp->f_pos + offset; 85 break; 86 case SEEK_END: 87 newpos = MEM_SIZE * sizeof(int)-1 + offset;/*從文件尾開始定位*/ 88 break; 89 default: 90 return -EINVAL; 91 } 92 93 if ((newpos<0) || (newpos>MEM_SIZE * sizeof(int)))/*檢查文件指針移動后位置是否正確*/ 94 return -EINVAL; 95 96 filp->f_pos = newpos; 97 return newpos; 98 99 } 100 101 const struct file_operations mem_ops = { 102 .llseek = mem_llseek, 103 .open = mem_open, 104 .read = mem_read, 105 .write = mem_write, 106 .release = mem_release, 107 }; 108 109 static int memdev_init(void){ 110 int ret = -1; 111 112 /*動態分配設備號*/ 113 ret = alloc_chrdev_region(&my_dev.devno,0,1,"memdev"); 114 if (ret >= 0){ 115 cdev_init(&my_dev.cdev,&mem_ops);/*初始化字符設備*/ 116 cdev_add(&my_dev.cdev,my_dev.devno,1);/*添加字符設備*/ 117 } 118 119 return ret; 120 } 121 122 static void memdev_exit(void){ 123 cdev_del(&my_dev.cdev); 124 unregister_chrdev_region(my_dev.devno,1); 125 126 } 127 128 module_init(memdev_init); 129 module_exit(memdev_exit);
l 驅動測試
經過上面的代碼我們已經實現了一個簡單的字符設備驅動,我們下面進行測試。(應用程序在https://github.com/wrjvszq/myblongs.git 上)
1. 加載內核模塊
我們使用 insmod memdev.ko 命令加載內核模塊
2. 獲取設備號
我們的設備號是動態申請到的,所以我們要通過下面的命令查看設備號
cat /proc/devices
找到我們的設備memdev的設備號
3. 建立設備文件
使用如下命令建立設備文件
mknod /dev/文件名 c 主設備號次設備號
上面命令中文件名為我們在應用程序中打開的文件名
c代表字符設備
主設備號為上一步找到的,我的位249
次設備號非負即可,但不能超過自己所創建的設備數。
比如我的就是 mknod /dev/memdev0 c 249 0
4. 編譯應用程序並測試
使用gcc對應用程序進行編譯,然后先使用write對設備進行寫入,在使用read對設備讀取,完成測試。