如何編寫一個簡單的Linux驅動(三)——完善裝置驅動

山無言發表於2020-09-13

前期知識

  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 //原始碼檔名為"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",讓使用者程式寫裝置,可以看到終端列印出了使用者程式傳遞給裝置的資訊。

  本文的全部程式碼在這裡

相關文章