linux驱动开发学习笔记五:如何开始编写你的第一个字符设备驱动?

一、驱动模块的加载和卸载

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_fopstest_fops就是设备的操作函数集 合,只是此时我们还没有初始化 test_fops中的 openrelease等这些成员变量,所以这个操作函数集合还是空的。

第 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.kormmod drv.ko这两个命令来加载和卸载.ko模块,也包括上述说的创建一个/lib/modules/4.1.15目录,并且还需要将chrdevbase.ko文件发送到开发板系统中,然后将该文件放到/lib/modules/4.1.15路径下即可
  • 执行cat /proc/devices命令查看当前已经被使用掉的设备号

那么自然剩余的操作都是在虚拟机的linux系统中完成的,例如Makefile中的内核目录,肯定是内核在虚拟机中的路径。但是如果你只使用一个系统,那么上述所有的操作肯定都是在这一个系统中来完成。

最后,关于如何编写应用程序去验证字符设备驱动程序,请查看我的下一篇博客如何编写应用程序去验证我们的字符设备驱动程序?

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

耐心的小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值