前期知識html
1.如何編寫一個簡單的Linux驅動(一)——驅動的基本框架
linux
2.如何編寫一個簡單的Linux驅動(二)——設備操做集file_operations
git
前言程序員
在上一篇文章中,咱們編寫設備驅動遇到了很多問題:github
(1) 註冊設備時,設備號須要程序員給定,每次編寫驅動時,程序員須要知道有哪些設備號是空閒的;框架
(2) 加載驅動後,須要用戶使用mknod命令手動生成設備節點;函數
(3) 雖然用戶程序調用了讀寫設備的函數,可是並無數據傳輸。spa
在本篇文章中,咱們會一次解決這三個問題。code
要下載上一篇文章所寫的所有代碼,請點擊這裏。htm
1.自定義一個設備結構體
爲了方便,咱們本身定義一個結構體,用於描述咱們的設備,存放和設備有關的屬性。打開上一篇文章所寫的源代碼文件,加入以下代碼。
1 struct shanwuyan_dev 2 { 3 struct cdev c_dev; //字符設備 4 dev_t dev_id; //設備號 5 struct class *class; //類 6 struct device *device; //設備 7 int major; //主設備號 8 int minor; //次設備號 9 }; 10 11 struct shanwuyan_dev shanwuyan; //定義一個設備結構體
咱們對成員變量分別進行解析。
成員變量 | 描述 |
struct cdev c_dev | 這是一個字符設備結構體,在後文咱們再介紹 |
dev_t dev_id | 這是一個32位的數據,其中高12位表示主設備號,低20位表示次設備號,高低設備號組合在一塊兒表示一個完整的設備號 |
struct class *class | 類,主要做用後文再介紹 |
struct device *device | 設備,主要做用後文再介紹 |
int major | 主設備號 |
int minor | 次設備號 |
接下來咱們要介紹三個宏函數"MAJOR"、"MINOR"、"MKDEV",它們的原型以下。
1 #define MINORBITS 20 2 #define MINORMASK ((1U << MINORBITS) - 1) 3 4 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 5 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 6 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
看起來很複雜,可是它們的功能很簡單:"MAJOR"的做用是根據設備號獲取主設備號,即設備號的高12位;"MINOR"的做用是根據設備號獲取次設備號,即設備號的低20位;"MKDEV"的做用是把主設備號和次設備號合併成一個完整的設備號。
2.新的註冊與註銷字符設備的方法
在上一篇文章中,咱們使用"register_chrdev"函數來註冊設備,使用"unregister_chrdev"函數來註銷設備。這一組函數的缺點是:首先,主設備號須要用戶給定;其次,使用該函數的話,設備會佔據整個主設備號,對應的次設備號沒法使用,形成設備號的浪費。爲了克服以上缺點,咱們引入兩組新的註冊設備號的函數"register_chrdev_region"和"alloc_chrdev_region",這兩個函數對應的註銷設備號的函數都是"unregister_chrdev_region"。它們的函數原型以下。
1 //這些函數的聲明都在linux/fs.h中 2 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); //第一個參數是設備號的地址,第二個參數是次設備號的起始號,第三個參數是要申請的個數,第四個參數是設備名稱 3 extern int register_chrdev_region(dev_t, unsigned, const char *); //第一個參數是設備號,第二個參數是要申請的個數,第三個參數是設備名稱 4 extern void unregister_chrdev_region(dev_t, unsigned); //第一個參數是設備號,第二個參數是申請的個數
若是用戶給定了主設備號,可使用"register_chrdev_region"函數來讓系統分配次設備號;若是用戶未給定主設備號,可使用"alloc_chrdev_region"函數,由系統分配主設備號和次設備號。這兩個函數在驅動的入口函數裏調用,做初始化用。相應的,要在驅動出口函數中調用"unregister_chrdev_region"函數來註銷設備號。以下方代碼。
1 static int __init shanwuyan_init(void) //驅動入口函數 2 { 3 int ret = 0; 4 5 shanwuyan.major = 0; //主設備號設置爲0,表示用戶不給定主設備號,主次設備號都由系統分配 6 /*1.分配設備號*/ 7 if(shanwuyan.major) //若是給定了主設備號,則由系統分配次設備號 8 { 9 shanwuyan.dev_id = MKDEV(shanwuyan.major, 0); //把用戶給的主設備號和0號次設備號合併成一個設備號 10 ret = register_chrdev_region(shanwuyan.dev_id, 1, SHANWUYAN_NAME); //由於咱們只考慮一個設備的狀況,因此只分配一個設備號,即設備號0 11 } 12 else //若是沒有給定主設備號,則主次設備號所有由系統分配 13 { 14 ret = alloc_chrdev_region(&(shanwuyan.dev_id), 0, 1, SHANWUYAN_NAME); //只考慮一個設備的狀況 15 shanwuyan.major = MAJOR(shanwuyan.dev_id); //獲取主設備號 16 shanwuyan.minor = MINOR(shanwuyan.dev_id); //獲取次設備號 17 } 18 if(ret < 0) //設備號分配失敗,則打印錯誤信息,而後返回 19 { 20 printk(KERN_EMERG "shanwuyan chrdev_region error!\r\n"); 21 return -EINVAL; 22 } 23 else //若是設備號分配成功,則打印設備的主次設備號 24 { 25 printk(KERN_EMERG "shanwuyan.major = %d, shanwuyan.minor = %d\r\n", shanwuyan.major, shanwuyan.minor); 26 } 27 28 29 return 0; 30 } 31 32 static void __exit shanwuyan_exit(void) //驅動出口函數 33 { 34 /*1.註銷設備號*/ 35 unregister_chrdev_region(shanwuyan.dev_id, 1); 36 }
以上代碼的功能是:入口函數實現由系統分配主次設備號,出口函數實現註銷系統分配的設備號。
聽起來這兩組新的註冊設備號的函數好處多多,可是它們卻有一個致命的缺點,那就是只能實現分配設備號的功能,卻沒法像"register_chrdev"函數那樣還能夠把設備添加到內核中。爲了把設備添加到內核,咱們就要引進字符設備結構體"struct cdev",這也是咱們文章開頭的自定義結構體的第一個成員變量。該結構體的原型以下。
1 //該結構體原型在linux/cdev.h中,記得在驅動代碼中包含進去 2 struct cdev { 3 struct kobject kobj; 4 struct module *owner; 5 const struct file_operations *ops; 6 struct list_head list; 7 dev_t dev; 8 unsigned int count; 9 };
在本文中,咱們只用到該結構體中的三個成員變量"struct module *owner"、"const struct file_operations *ops"、"dev_t dev",他們的描述以下。
成員變量 | 描述 |
struct module *owner |
通常取值爲THIS_MODULE |
const struct file_operations *ops |
設備操做集file_operations的地址 |
dev_t dev |
就是設備號 |
接下來要介紹兩個與該結構體相關的函數,"cdev_init"和"cdev_add",它們的原型以下。
1 void cdev_init(struct cdev *, const struct file_operations *); //第一個參數是struct cdev結構體變量的地址,第二個參數是字符設備操做集的地址 2 int cdev_add(struct cdev *, dev_t, unsigned); //第一個參數是struct cdev結構體變量的地址,第二個參數是設備號,第三個參數是要添加的數量
這兩個函數的做用分別是初始化字符設備結構體和向內核添加字符設備。
向入口函數中添加代碼,將字符設備註冊到內核中,添加的代碼以下。
1 static int __init shanwuyan_init(void) //驅動入口函數 2 { 3 int ret = 0; 4 5 /*1.分配設備號*/ 6 ... 7 8 /*2.向內核添加字符設備*/ 9 shanwuyan.c_dev.owner = THIS_MODULE; 10 cdev_init(&(shanwuyan.c_dev), &(shanwuyan_fops)); //初始化字符設備結構體 11 cdev_add(&(shanwuyan.c_dev), shanwuyan.dev_id, 1); //添加設備到內核 12 13 return 0; 14 }
這樣,設備就註冊成功了。
3.自動建立設備節點
要實現自動建立設備節點,咱們須要引進兩個結構體,"struct class"和"struct device"。即,文章開頭的自定義設備結構體中的成員變量"struct class *class"和"struct device *device"是用於實現自動生成設備節點的。這兩個結構體的具體實現咱們先不做深刻了解,只須要了解如何在這裏使用他們。咱們先引進四個關於這兩個結構體的函數,"class_create"、"class_destroy"、"device_create"、"device_destroy",這些函數的做用分別是建立類、摧毀類、建立設備、摧毀設備。它們的原型以下。
1 //位於"linux/device.h"中,記得在驅動代碼中包含進去 2 #define class_create(owner, name) \ //第一個參數是全部者(通常爲THIS_MODULE),第二個參數是設備名稱 3 ({ \ 4 static struct lock_class_key __key; \ 5 __class_create(owner, name, &__key); \ 6 }) 7 8 extern void class_destroy(struct class *cls); //參數是建立的類的地址 9 10 struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); //第一個參數是類的地址,第二個參數爲父設備地址(通常爲NULL),第三個參數爲設備號,第四個參數爲可能用到的數據(通常爲NULL),第五個參數爲設備名稱 11 extern void device_destroy(struct class *cls, dev_t devt); //第一個參數爲類的地址,第二個參數爲設備號
爲了實現自動建立設備節點,咱們要在入口函數中建立一個類,而後在類裏建立一個設備。在出口函數中,也要相應地摧毀設備和類。代碼以下。
1 static int __init shanwuyan_init(void) //驅動入口函數 2 { 3 int ret = 0; 4 5 /*1.分配設備號*/ 6 ... 7 8 /*2.向內核添加字符設備*/ 9 ... 10 11 /*3.自動建立設備節點*/ 12 shanwuyan.class = class_create(THIS_MODULE, SHANWUYAN_NAME); //建立類 13 shanwuyan.device = device_create(shanwuyan.class, NULL, shanwuyan.dev_id, NULL, SHANWUYAN_NAME); //建立設備,設備節點就自動生成了。正常狀況下,要考慮類和設備建立失敗的狀況,爲了簡化代碼,這裏就不寫了 14 15 return 0; 16 } 17 18 static void __exit shanwuyan_exit(void) //驅動出口函數 19 { 20 /*1.註銷設備號*/ 21 ... 22 /*2.摧毀設備*/ 23 device_destroy(shanwuyan.class, shanwuyan.dev_id); 24 /*3.摧毀類*/ 25 class_destroy(shanwuyan.class); 26 }
在入口函數中,咱們先建立了類,後建立了設備,即有類纔能有設備,因此在出口函數中,咱們要先把設備摧毀了,而後再摧毀類。
4.實現與用戶程序的數據傳輸
上一篇文章中,file_operations的讀寫操做並無發揮真正的做用。在本文中,咱們改寫一下驅動讀寫函數和用戶程序代碼,讓設備和用戶程序實現數據傳輸。
首先修改一下驅動程序的"shanwuyan_write"函數和"shanwuyan_read"函數,其中讀函數的做用是向用戶程序傳輸一個字符串,寫函數的做用是接收用戶程序發來的數據,並打印出來,代碼以下。
1 /*讀設備*/ 2 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) 3 { 4 char device_data[] = "device data"; 5 copy_to_user(buf, device_data, sizeof(device_data)); //向用戶程序傳輸設備數據 6 return 0; 7 } 8 9 /*寫設備*/ 10 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) 11 { 12 char user_data[50]; 13 copy_from_user(user_data, buf, count); //獲取用戶程序寫到設備的數據 14 printk("device get data:%s\r\n", user_data); 15 return 0; 16 }
這裏用到了兩個函數,"copy_to_user"和"copy_from_user",做用分別是向用戶程序傳輸數據和從用戶程序接收數據。它們的原型以下。
1 //聲明在文件linux/uaccess.h中,記得在驅動代碼中包含進去 2 static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) //第一個參數是目的地址,第二個參數是源地址,第三個參數是數據的size 3 static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) //第一個參數是目的地址,第二個參數是源地址,第三個參數是數據的size
接下來改造用戶程序,所有代碼以下。
1 //源代碼文件名爲"shanwuyanAPP.c" 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <stdio.h> 6 #include <unistd.h> 7 #include <stdlib.h> 8 #include <string.h> 9 10 /* 11 *argc:應用程序參數個數,包括應用程序自己 12 *argv[]:具體的參數內容,字符串形式 13 *./shanwuyanAPP <filename> <r:w> r表示讀,w表示寫 14 */ 15 int main(int argc, char *argv[]) 16 { 17 int ret = 0; 18 int fd = 0; 19 char *filename; 20 char readbuf[50]; 21 char user_data[] = "user data"; 22 23 if(argc != 3) 24 { 25 printf("Error usage!\r\n"); 26 return -1; 27 } 28 29 filename = argv[1]; //獲取文件名稱 30 31 fd = open(filename, O_RDWR); 32 if(fd < 0) 33 { 34 printf("cannot open file %s\r\n", filename); 35 return -1; 36 } 37 /*讀操做*/ 38 if(!strcmp(argv[2], "r")) 39 { 40 read(fd, readbuf, 50); 41 printf("user get data:%s\r\n", readbuf); 42 } 43 /*寫操做*/ 44 else if(!strcmp(argv[2], "w")) 45 { 46 write(fd, user_data, 50); 47 } 48 else 49 { 50 printf("ERROR usage!\r\n"); 51 } 52 53 /*關閉操做*/ 54 ret = close(fd); 55 if(ret < 0) 56 { 57 printf("close file %s failed\r\n", filename); 58 } 59 60 return 0; 61 }
5.應用
編譯驅動程序,交叉編譯用戶程序,拷貝到開發板中。
在終端輸入命令"insmod shanwuyan.ko"加載驅動,能夠看到系統分配的主次設備號分別爲246和0.
在終端輸入命令"ls /dev/shanwuyan",能夠看到已經自動建立了設備節點"/dev/shanwuyan"。
在終端輸入"./shanwuyanAPP /dev/shanwuyan r",讓用戶程序讀設備,能夠看到終端打印出了設備傳遞給用戶程序的信息。
在終端輸入"./shanwuyanAPP /dev/shanwuyan w",讓用戶程序寫設備,能夠看到終端打印出了用戶程序傳遞給設備的信息。
本文的所有代碼在這裏。