如何編寫一個簡單的Linux驅動(二)——完善設備驅動
前期知識
1.如何編寫一個簡單的Linux驅動(一)——驅動的基本框架
2.如何編寫一個簡單的Linux驅動(二)——設備操作集file_operations
前言
在上一篇文章中,我們編寫設備驅動遇到了不少問題:
(1) 註冊設備時,設備號需要程式設計師給定,每次編寫驅動時,程式設計師需要知道有哪些設備號是空閑的;
(2) 載入驅動後,需要用戶使用mknod命令手動生成設備節點;
(3) 雖然用戶程式調用了讀寫設備的函數,但是並沒有數據傳輸。
在本篇文章中,我們會一次解決這三個問題。
要下載上一篇文章所寫的全部程式碼,請點擊這裡。
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 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <unistd.h> 6 #include <stdlib.h> 7 #include <string.h> 8 9 /* 10 *argc:應用程式參數個數,包括應用程式本身 11 *argv[]:具體的參數內容,字元串形式 12 *./shanwuyanAPP <filename> <r:w> r表示讀,w表示寫 13 */ 14 int main(int argc, char *argv[]) 15 { 16 int ret = 0; 17 int fd = 0; 18 char *filename; 19 char readbuf[50]; 20 char user_data[] = "user data"; 21 22 if(argc != 3) 23 { 24 printf("Error usage!\r\n"); 25 return -1; 26 } 27 28 filename = argv[1]; //獲取文件名稱 29 30 fd = open(filename, O_RDWR); 31 if(fd < 0) 32 { 33 printf("cannot open file %s\r\n", filename); 34 return -1; 35 } 36 /*讀操作,從設備中讀取字元串,並列印出來*/ 37 if(!strcmp(argv[2], "r")) 38 { 39 read(fd, readbuf, 50); 40 printf("user get data:%s\r\n", readbuf); 41 } 42 /*寫操作,向設備寫字元串*/ 43 else if(!strcmp(argv[2], "w")) 44 { 45 write(fd, user_data, 50); 46 } 47 else 48 { 49 printf("ERROR usage!\r\n"); 50 } 51 52 /*關閉操作*/ 53 ret = close(fd); 54 if(ret < 0) 55 { 56 printf("close file %s failed\r\n", filename); 57 } 58 59 return 0; 60 }
5.應用
編譯驅動程式,交叉編譯用戶程式,拷貝到開發板中。
在終端輸入命令”insmod shanwuyan.ko”載入驅動,可以看到系統分配的主次設備號分別為246和0.
在終端輸入命令”ls /dev/shanwuyan”,可以看到已經自動創建了設備節點”/dev/shanwuyan”。
在終端輸入”./shanwuyanAPP /dev/shanwuyan r”,讓用戶程式讀設備,可以看到終端列印出了設備傳遞給用戶程式的資訊。
在終端輸入”./shanwuyanAPP /dev/shanwuyan w”,讓用戶程式寫設備,可以看到終端列印出了用戶程式傳遞給設備的資訊。
本文的全部程式碼在這裡。