昨天咱們對linux內核的子系統進行簡單的認識,今天咱們正式進入驅動的開發,咱們從此的學習爲了不你們沒有硬件的缺陷,咱們都會以虛擬的設備爲例進行學習,因此你們沒必要懼怕沒有硬件的問題。linux
今天咱們會分析到如下內容:git
1. 字符設備驅動基礎程序員
2. 簡單字符設備驅動實現github
3. 驅動測試app
1. 字符設備描述結構async
在linux2.6內核中,使用cdev結構體描述一個字符設備,其定義以下:ide
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對設備讀取,完成測試。