一、驱动模块的加载和卸载
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko
),在Linux 内核启动以后使用“insmod”
命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init
函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init
就是需要注册的具体函数,当使用“insmod”
命令加载驱动的时候,xxx_init
这个函数就会被调用。module_exit()
函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit
就是需要注册的具体函数,当使用“rmmod”
命令卸载具体驱动的时候 xxx_exit
函数就会被调用。
字符设备驱动模块加载
和卸载
模板如下所示:
1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7
8 /* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
第 2 行
,定义了个名为 xxx_init
的驱动入口函数,并且使用了“__init”
来修饰。
第 9 行
,定义了个名为xxx_exit
的驱动出口函数,并且使用了“__exit”
来修饰。
第 15 行
,调用函数 module_init
来声明 xxx_init
为驱动入口函数,当加载驱动的时候xxx_init
函数就会被调用。
第 16 行
,调用函数module_exit
来声明xxx_exit
为驱动出口函数,当卸载驱动的时候xxx_exit
函数就会被调用。
驱动的加载
和卸载
推荐分别使用:
modprobe drv.ko
rmmod drv.ko
之所以不使用insmod
命令,是因为insmod
命令不能解决模块的依赖关系,比如 drv.ko
依赖 first.ko
这个模块,就必须先使用insmod
命令加载first.ko
这个模块,然后再加载 drv.ko
这个模块。但是 modprobe
就不会存在这个问题,modprobe
会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe
命令相比insmod
要智能一些。modprobe
命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,所以推荐使用 modprobe
命令来加载驱动。modprobe
命令默认会去/lib/modules/<kernel-version>
目录中查找模块,我们使用的 Linux kernel
的版本号为 4.1.15
,因此 modprobe
命令默认会到/lib/modules/4.1.15
这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
二、字符设备的注册和注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev
函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
- major:主设备号,Linux 下每个设备都有一个设备号,设备号分为
主设备号
和次设备号
两部分,关于设备号后面会提到。 - name:设备名字,类型为字符串指针,指向一串字符串。
- fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev
函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
- major:要注销的设备对应的主设备号。
- name:要注销的设备对应的设备名
一般字符设备的注册在驱动模块的入口函数 xxx_init
中进行,字符设备的注销在驱动模块的出口函数 xxx_exit
中进行。如下所示:
1 static struct file_operations test_fops;
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6 /* 入口函数具体内容 */
7 int retvalue = 0; 89 /* 注册字符设备驱动 */
10 retvalue = register_chrdev(200, "chrtest", &test_fops);
11 if(retvalue < 0){
12 /* 字符设备注册失败,自行处理 */
13 }
14 return 0;
15 }
16
17 /* 驱动出口函数 */
18 static void __exit xxx_exit(void)
19 {
20 /* 注销字符设备驱动 */
21 unregister_chrdev(200, "chrtest");
22 }
23
24 /* 将上面两个函数指定为驱动的入口和出口函数 */
25 module_init(xxx_init);
26 module_exit(xxx_exit);
第 1 行
,定义了一个 file_operations
结构体变量 test_fops
,test_fops
就是设备的操作函数集 合
,只是此时我们还没有初始化 test_fops
中的 open
、release
等这些成员变量,所以这个操作函数集合还是空的。
第 10 行,
调用函数 register_chrdev
注册字符设备,主设备号为200
,设备名为“chrtest”,
设备操作函数集合就是第 1 行定义的test_fops
。要注意的一点就是,选择没有被使用的主设备号,输入命令“cat /proc/devices”
可以查看当前已经被使用掉的设备号。注意因为开发程序和执行可能不在同一个系统中,例如我们在虚拟机中写好程序,然后去板子上的系统中去执行,那么此时你应该查看的是你板子的系统中没有被使用的主设备号。
三、实现设备的具体操作函数
这里通俗来讲,就是实现file_operations
结构体中的一些具体的操作函数,如open,read,write、release
等。
file_operations
结构体的内容如下:
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *,
unsigned long,unsigned long,
unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *,
struct pipe_inode_info *,
size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void **);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
1616 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1617 #ifndef CONFIG_MMU
1618 unsigned (*mmap_capabilities)(struct file *);
1619 #endif
1620 };
具体函数实现代码如下:
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
7
8 /* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
10 {
11 /* 用户实现具体功能 */
12 return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
17 {
18 /* 用户实现具体功能 */
19 return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用户实现具体功能 */
26 return 0;
27 }
28
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
36
37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函数具体内容 */
41 int retvalue = 0;
42
43 /* 注册字符设备驱动 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符设备注册失败,自行处理 */
47 }
48 return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注销字符设备驱动 */
55 unregister_chrdev(200, "chrtest");
56 }
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
对于以上定义的结构体test_fops
我是这么理解的:首先我们先创建一个file_operations
类型的结构体test_fops
,然后我们如果要实现其中的某个函数,那么我们就需要以上述代码中的格式去声明,也即在自己定义的结构体中给对应的属性去赋值,赋值的内容就是我们要在驱动程序中实现的对应函数
的函数名。例如,我们此时在应用程序调用open
,那么最终会调用到我们的chrtest_open,
这就实现了应用程序对驱动程序的访问。也即file_operations
里的函数是应用程序调用的,我们在上述代码中定义的对应操作函数是在驱动程序里的,最终我们通过给file_operations
类型的结构体test_fops
的属性赋值的方式,将这两种操作函数,也即将应用程序
中的操作函数和驱动程序
中的操作函数联系在了一起。
四、添加 LICENSE 和作者信息
最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
......
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
61
62 MODULE_LICENSE("GPL");
63 MODULE_AUTHOR("alex");
至此,字符设备驱动开发的完整步骤就讲解完了,而且也编写好了一个完整的字符设备驱动模板,以后字符设备驱动开发都可以在此模板上进行。
最后完整的代码如下:
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h> 7
17
18 #define CHRDEVBASE_MAJOR 200 /* 主设备号 */
19 #define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
20
21 static char readbuf[100]; /* 读缓冲区 */
22 static char writebuf[100]; /* 写缓冲区 */
23 static char kerneldata[] = {"kernel data!"};
24
25 /*
26 * @description : 打开设备
27 * @param – inode : 传递给驱动的 inode
28 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
29 * 一般在 open 的时候将 private_data 指向设备结构体。
30 * @return : 0 成功;其他 失败
31 */
32 static int chrdevbase_open(struct inode *inode, struct file *filp)
33 {
34 printk("chrdevbase open!\r\n");
35 return 0;
36 }
37
38 /*
39 * @description : 从设备读取数据
40 * @param - filp : 要打开的设备文件(文件描述符)
41 * @param - buf : 返回给用户空间的数据缓冲区
42 * @param - cnt : 要读取的数据长度
43 * @param - offt : 相对于文件首地址的偏移
44 * @return : 读取的字节数,如果为负值,表示读取失败
45 */
46 static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
47 {
48 int retvalue = 0;
49
50 /* 向用户空间发送数据 */
51 memcpy(readbuf, kerneldata, sizeof(kerneldata));
52 retvalue = copy_to_user(buf, readbuf, cnt);
53 if(retvalue == 0){
54 printk("kernel senddata ok!\r\n");
55 }
else{
56 printk("kernel senddata failed!\r\n");
57 }
58
59 printk("chrdevbase read!\r\n");
60 return 0;
61 }
62
63 /*
64 * @description : 向设备写数据
65 * @param - filp : 设备文件,表示打开的文件描述符
66 * @param - buf : 要写给设备写入的数据
67 * @param - cnt : 要写入的数据长度
68 * @param - offt : 相对于文件首地址的偏移
69 * @return : 写入的字节数,如果为负值,表示写入失败
70 */
71 static ssize_t chrdevbase_write(struct file *filp,const char __user *buf,
size_t cnt, loff_t *offt)
72 {
73 int retvalue = 0;
74 /* 接收用户空间传递给内核的数据并且打印出来 */
75 retvalue = copy_from_user(writebuf, buf, cnt);
76 if(retvalue == 0){
77 printk("kernel recevdata:%s\r\n", writebuf);
78 }
else{
79 printk("kernel recevdata failed!\r\n");
80 }
81
82 printk("chrdevbase write!\r\n");
83 return 0;
84 }
85
86 /*
87 * @description : 关闭/释放设备
88 * @param - filp : 要关闭的设备文件(文件描述符)
89 * @return : 0 成功;其他 失败
90 */
91 static int chrdevbase_release(struct inode *inode,struct file *filp)
92 {
93 printk("chrdevbase release!\r\n");
94 return 0;
95 }
96
97 /*
98 * 设备操作函数结构体
99 */
100 static struct file_operations chrdevbase_fops = {
101 .owner = THIS_MODULE,
102 .open = chrdevbase_open,
103 .read = chrdevbase_read,
104 .write = chrdevbase_write,
105 .release = chrdevbase_release,
106 };
107
108 /*
109 * @description : 驱动入口函数
110 * @param : 无
111 * @return : 0 成功;其他 失败
112 */
113 static int __init chrdevbase_init(void)
114 {
115 int retvalue = 0;
116
117 /* 注册字符设备驱动 */
118 retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
&chrdevbase_fops);
119 if(retvalue < 0){
120 printk("chrdevbase driver register failed\r\n");
121 }
122 printk("chrdevbase_init()\r\n");
123 return 0;
124 }
125
126 /*
127 * @description : 驱动出口函数
128 * @param : 无
129 * @return : 无
130 */
131 static void __exit chrdevbase_exit(void)
132 {
133 /* 注销字符设备驱动 */
134 unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
135 printk("chrdevbase_exit()\r\n");
136 }
137
138 /*
139 * 将上面两个函数指定为驱动的入口和出口函数
140 */
141 module_init(chrdevbase_init);
142 module_exit(chrdevbase_exit);
143
144 /*
145 * LICENSE 和作者信息
146 */
147 MODULE_LICENSE("GPL");
148 MODULE_AUTHOR("zuozhongkai");
需要注意的是,因为驱动程序的编写属于内核编程,因此包含的头文件都是内核源码里的一些头文件,而不是linux系统中编写应用程序的时候使用的C库
。举个例子,我们的打印函数,用的是printk
而不是printf
,但是两者的功能是很相似的。
五、最后,我们就可以对程序进行编译,从而生成.ko
文件。这一步最重要的是Makefile
文件的编写
KERNELDIR := “你的内核源码所在的目录,也即是arch文件夹的上一级目录”
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 1 行
,KERNELDIR
表示 Linux 内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。
第 3 行
,CURRENT_PATH
表示当前路径,直接通过运行pwd
命令来获取当前所处路径。
第 5 行
,obj-m
表示将 chrdevbase.c
这个文件编译为 chrdevbase.ko
模块。
第 10 行
,具体的编译命令,后面的 modules
表示编译模块,-C
表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR
目录。M
表示模块源码目录,make modules
命令中加入 M=dir
以后程序会自动到指定的 dir
目录中读取模块的源码并将其编译为.ko
文件。
然后我们在该设备驱动程序所在的路径下,也即chrdevbase.c
的同级路径下,输入make
命令,就可以进行编译,编译完成之后,就会生成chrdevbase.ko
文件。
六、注释:
下面假设我们使用虚拟机来开发程序,然后使用开发板上的linux系统来执行,那么在上面所说的一些操作中,需要在开发板上的linux系统操作的是:
- 执行
modprobe drv.ko
,rmmod drv.ko
这两个命令来加载和卸载.ko
模块,也包括上述说的创建一个/lib/modules/4.1.15
目录,并且还需要将chrdevbase.ko
文件发送到开发板系统中,然后将该文件放到/lib/modules/4.1.15
路径下即可 - 执行
cat /proc/devices
命令查看当前已经被使用掉的设备号
那么自然剩余的操作都是在虚拟机的linux系统中完成的,例如Makefile
中的内核目录,肯定是内核在虚拟机中的路径。但是如果你只使用一个系统,那么上述所有的操作肯定都是在这一个系统中来完成。
最后,关于如何编写应用程序去验证字符设备驱动程序,请查看我的下一篇博客:如何编写应用程序去验证我们的字符设备驱动程序?