驱动代码架构分析
驱动程序与内核模块的编写有着共通之处,驱动程序在内核模块的基础上补充添加更为完善的调用响应。驱动的执行过程为:
- 应用程序使用库提供的
open(dev_name, mode)
函数打开dev_name
设备文件; - 库根据传入参数执行
swi
指令,这条指令将会引发CPU异常从而进入内核; - 内核的异常处理函数根据所提供的参数查找相应的驱动程序;
- 执行驱动程序;
- 返回一个文件句柄给库,进而返回给应用程序。
其他库函数read()
、write()
等的执行过程类似。因此,要完成驱动程序的编写,实际上主要要完成对驱动所支持的各函数的编写。驱动所支持的函数调用结构体如下,仅选取部分重要的内容显示:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
...
};
struct file_operations
将字符设备驱动的操作和设备号联系起来,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,用来执行一系列的系统调用。
因此笔者最终完成的驱动所使用的该结构体声明如下:
static struct file_operations rw_fops = {
.owner = THIS_MODULE,
.open = rw_open,
.release = rw_release,
.read = rw_read,
.write = rw_write,
};
结构体内,等号左边的即为系统调用,等号右边的即为自定义的对应函数。
除此之外,驱动在装载和卸载阶段也会执行一些操作,这些操作与内核模块编写大致相同,但其针对不同的驱动进行了更有针对性的补充完善。
首先,查看文件/proc/devices
,选择一个没有在该文件中声明的变量号作为主设备号,笔者选用了369
号,同时完成对设备的命名、版权声明等。主设备号的范围要求在0-4095
之间,即unsigned int
型的高12位。具体如下:
/* 指定一个0-4095范围(usigned int的高12位)内的设备号,并给设备命名 */
#define RW_MAJOR 369
#define DEVICE_NAME "RW_Module"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yige LIU");
MODULE_DESCRIPTION("MY Read And Write Module");
接着详细描述自定义的操作响应函数的功能及实现。
init()
在驱动装载阶段执行该函数内容。该函数主要完成对设备号的申请以及对所需空间的申请工作,具体代码如下:
static int __init rw_init(void){ //装载驱动
int ret = register_chrdev(RW_MAJOR, DEVICE_NAME, &rw_fops);
if (ret < 0) {
printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
return ret;
}
rw_devp = kzalloc(sizeof(RW_DEV), GFP_KERNEL); //申请空间
printk(KERN_EMERG DEVICE_NAME " Initialized.\n");
return 0;
}
module_init(rw_init);
exit()
在卸载驱动阶段将执行该函数。该函数动作与装载阶段执行的init()
的动作恰好相反且对应,主要完成对设备号的回收以及对空间的回收,具体代码如下:
static void __exit rw_exit(void){ //卸载驱动
unregister_chrdev(RW_MAJOR, DEVICE_NAME);
kfree(rw_devp); //释放空间
printk(KERN_EMERG DEVICE_NAME " Removed.\n");
}
module_exit(rw_exit);
驱动功能分析及实现
该驱动对应的设备提供给用户一个大小为32的使用空间,当用户发起写操作时,将会向指定设备的空闲空间中写入两个int
类型的数,这两个数由用户定义;当用户发起读操作时,将会从最先写入的空间中读取两个数据,并返回二者中较大的数给用户。
据此定义该驱动设备对应的结构体。其中,num[]
数组用来存储用户写入的数据;flag[]
数组用来标记对应空间是否可用;blank_pos
用来记录第一个空闲空间位置;full_pos
用来记录当前该读取的空间位置。同时声明全局结构体指针,以便驱动后续操作。具体代码如下:
#define BUFFER_SIZE 32
typedef struct rw_dev{
int num[BUFFER_SIZE];
int flag[BUFFER_SIZE]; //标记是否存储了值,1表示存储,0表示空
int blank_pos; //从左向右第一个空闲块号
int full_pos; //从左向右标记该读取的位置
}RW_DEV;
RW_DEV *rw_devp = NULL;
open()
open()
函数完成对驱动设备的初始化,将所有空间指定为空闲空间,并将blank_pos
与full_pos
指向数组起始位置。同时,该函数还完成了对file
指针private_data
的赋值,使其永远指向设备起始结构体。具体代码如下:
static int rw_open(struct inode *inode, struct file *file){
file->private_data = rw_devp;
for (int i = 0; i < BUFFER_SIZE; ++i) { //初始化buffer
rw_devp->flag[i] = 0;
}
rw_devp->blank_pos = 0; //初始化首个可用空间位置
rw_devp->full_pos = 0; //初始化该读取的位置
printk(KERN_EMERG "Device Open.\n");
return 0;
}
write()
write()
函数实现将用户输入的两个数存入设备缓冲区中并等待被读取。当缓冲区已被占满时,返回ERROR
以提示用户缓冲区已满,无法进行写操作,应先利用读操作释放被占用的缓冲区后再进行操作;否则返回OK
。
对于写操作,由于缓冲区大小为32,因此笔者采用了循环缓冲区,即当缓冲区占满后,下一次进行写操作的位置必然为当前所读的位置。在代码实现上使用blank_pos
标记当前应该被写的位置,full_pos
标记应该被读的位置。对于两个标记,每执行一次操作后自增1或2,并进行模32操作以限定访问范围并形成循环。
同时,由于驱动程序运行在内核空间,其作为内核的一部分被内核所信任,因此直接用户所提供的变量指针不能直接传给驱动程序,否则可能会引起系统崩溃等安全问题。因此Linux提供了两个函数copy_to_user()
和copy_from_user()
来分别将内核空间数据拷贝给用户空间和将用户空间的数据拷贝给内核空间。故笔者在获取用户数据和传输内核数据时使用了这两个函数。
具体代码如下:
static ssize_t rw_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){ //写两个数
RW_DEV *devp = file->private_data;
int pos = devp->blank_pos; //获取当前可用buffer的位置
if (devp->flag[pos] == 1) return ERROR; //当前位置被占用,无法写入数据
int ret = copy_from_user(&devp->num[pos], buf, sizeof(int)); //获取用户传入数据
if (ret != 0) return ERROR;
devp->flag[pos] = 1; //标记存在值
devp->blank_pos = (devp->blank_pos + 1) % BUFFER_SIZE; //循环递增blank_pos值
printk(KERN_EMERG "Device Write.\n");
return OK;
}
read()
read()
函数实现从设备缓冲区中依次读取两个数,计算两者中较大值并返回给用户。当缓冲区内不存在数据时,提示用户应先写入数据后再读取。否则从full_pos
标记的位置连续读取两个数,计算较大值并调用copy_to_user()
将数据传给用户空间。随后更改flag
标记将这两个空间从占用转为非占用状态,同时完成full_pos
的循环自增。具体代码如下:
static ssize_t rw_read(struct file *file, char __user *buf, size_t count, loff_t *ppos){ //读取最大值
RW_DEV *devp = file->private_data;
int pos = devp->full_pos; //获取应读取的位置
if (devp->flag[pos] == 0) return ERROR; //该位置没有值
int res = (devp->num[pos] >= devp->num[pos + 1]) ? devp->num[pos] : devp->num[pos + 1]; //计算较大值
for (int i = pos; i < pos + 2; ++i) devp->flag[i] = 0; //释放占用
devp->full_pos = (devp->full_pos + 2) % BUFFER_SIZE; //更新读取标志
int ret = copy_to_user(buf, &res, sizeof(int));
if (ret != 0) return ERROR;
printk(KERN_EMERG "Device Read.\n");
return OK;
}
release()
release()
函数没有对内核空间进行实质上的修改,仅输出调用信息,如下:
static int rw_release(struct inode *inode, struct file *filp){
printk(KERN_EMERG "Device Release.\n");
return OK;
}
Makefile的编写及执行
使用make
完成对驱动的生成,编写make文件如下:
ifneq ($(KERNELRELEASE),)
obj-m := rw.o
else
KDIR := /usr/src/linux-headers-$(shell uname -r)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
endif
在执行该make
的过程中,会跳转至KDIR
所指定的路径并执行该路径下的Makefile
文件。由于笔者使用的是WSL2
环境,(WSL2下驱动创建)该目录下的Makefile
中的CC
采用的是gnu89
标准,不支持for
循环结构,因此需要修改该路径下的Makefile
文件,在CC
中添加-std=gnu99
使其支持for
循环结构,如下:
接着在该目录下重新执行make
操作更新相关文件。待执行完成后,再回到驱动文件所在文件夹下执行make
即可,如下:
此时驱动文件目录下已产生驱动装载相关文件,如下:
创建设备文件
仅有驱动仍无法正常使用,还需要创建一个与该驱动相关联的设备文件。使用mknod
在/dev
目录下创建一个名为rw_dev
的文件,其设备类型为字符型设备,主设备号与该驱动对应的主设备号相同,次设备号指定为0,如下:
mknod
命令用于创建设备文件,参数为:设备文件名、设备类型(b/c)、主设备号、次设备号。b
表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;c
表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符。
编写驱动测试文件
首先调用库函数open()
打开与驱动相关的设备文件,当成功打开后根据用户指定的操作完成write
和read
操作,直到用户要求退出;同时对内核空间处理的结果进行显示,当无法完成处理时提示用户失败的原因为空间不足或无可用空间等。具体代码如下:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main(){
int fd = open("/dev/rw_dev", O_RDWR); //以可读写方式打开设备文件
if (fd < 0) {
printf("Open failed.\n");
return -1;
}
//指令格式
printf("ENTER THE FOLLOWING TO TEST:\n");
printf(" q --quit quit test\n");
printf(" r --read read the larger of 'a' and 'b'\n");
printf(" w a b --write write integer num to variable 'a' and 'b'\n");
printf("START THE TEST.\n");
char ch;
int a, b, res;
int ret1, ret2;
while(1){
printf("Command:");
ch = getchar();
if(ch == 'q') break; //退出
else if(ch == 'r'){ //读取较大值
ret1 = read(fd, &res, sizeof(int));
if (ret1) printf("The large one is : %d\n", res);
else printf("Memory is empty, please write it first.\n");
}
else if(ch == 'w'){ //写入两个数据
scanf(" %d %d", &a, &b);
ret1 = write(fd, &a, sizeof(int));
ret2 = write(fd, &b, sizeof(int));
if (ret1 && ret2) printf("Finish write.\n");
else printf("Memory occupied, please read it first.\n");
}
else printf("Format error, please re-enter.\n"); //其他非法的指令格式
while((ch = getchar()) != '\n') ; //读掉多余的参数或字符
putchar('\n');
}
return 0;
}
驱动装卸载及测试
使用sudo insmod rw.ko
装载该驱动。
对可能发生的情况进行测试,此处需要使用sudo
权限。
观察到,对于第一个r
命令,由于此时驱动设备空间内还没有数据,因此提示用户先写入数据;第一个w
命令向驱动设备空间中写入两个数据13和369;下一条命令test
并不存在因此提示用户命令格式错误要求重新输入;第二个w
命令向驱动设备空间中写入两个数据99和66;第二个r
命令在执行时,缓存中已存在两组数据,因此它读取第一组数据并计算两者中的较大值即369,并返回给用户程序以完成输出;第三个r
命令执行时内存中有一组数据,即w
输入的第二组数据,它计算两者中较大值即99并返回;q
命令退出该驱动测试程序。
最后使用sudo rmmod rw
卸载该驱动。
查看内核的输出,如下:
观察到两者保持一致。
参考资料
[1]. https://www.cnblogs.com/eleclsc/p/11533682.html
[2]. https://blog.csdn.net/xiaodingqq/article/details/80150347