一、Linux内核驱动的访问
1.内核空间实现
要使得用户空间的应用程序能够访问内核驱动模块,那么只能通过/dev目录下的设备节点进行访问。
通过module_init(dev_init);声明,将static int __init dev_init(void)函数作为此设备驱动的入口初始化函数,其完成(1)初始化字符设备cdev;(2)申请设备号,在这里提供了两种申请的方式(静态申请和动态申请);(3)将字符设备mdev和设备号devno进行注册。注意:在这里并没有创建设备文件。
字符设备mdev通用的可以进行静态申请和动态申请,在本程序Demo中使用了静态申请的方式,即直接定义全局变量struct cdev mdev;。设备号通用也是通过全局变量devno存储,即为static unsigned int devno = 0;。申请设备号的同时,也要为用户空间/dev目录下的设备节点进行命名,设备节点名为"dev_module"。
同样的,当设备驱动模块别卸载时,static void __exit dev_exit(void)函数作为出口。
通过module_exit(dev_exit);声明,将static void __exit dev_exit(void)函数作为设备驱动模块卸载后的出口函数。其完成两个功能,将所申请到是字符设备mdev删除,并且释放申请到的设备号devno。
在进行字符设备的注册时,使用的cdev_init函数其原型为:
从上图函数原型可知,需要传递参数有:字符设备cdev结构和所需要实现操作的方法函数集fops。在代码中字符设备结构为mdev,方法函数集为dev_fops,定义如下:
如上图可知,成员owner的取值表示本设备模块所拥有。其他成员分别为dev_open实现打开设备,dev_read实现读取设备数据,dev_write实现写数据,dev_close实现关闭设备。他们分别对应static struct file_operations结构的相同属性成员方法。具体方法实现如下:
如上图所示,因为目前并未明确的操作硬件设备,所以只是实现了相应方法的框架,然后通过打印的方式将调试信息输出进行调试。
当在用户空间的用户程序中调用open函数打开设备节点/dev/dev_module时,将会在内核空间中调用dev_open函数。当在用户空间中使用read函数操作/dev/dev_module设备节点文件时,同样的在内核空间中会调用dev_read函数执行操作。在static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)函数中实现了当用户空间使用read函数访问时,将一个整型数据值1234传递回用户空间。数据传递的方法使用copy_to_user函数进行拷贝(从内核空间拷贝数据到用户空间)。
如上图所示,实现了设备驱动的写操作和关闭操作。当在用户空间中调用write函数操作/dev/dev_module设备文件时,那么对应的在内核空间执行dev_write函数调用。dev_write函数实现了将用户空间写入的时间读取出来,然后进行打印。当在用户空间调用close函数通用的将会在内核空间调用执行dev_close函数,进行关闭设备。
驱动程序完成后,通过编译生成文件acce驱动文件。将其拷贝到板卡的根文件系统中,通过命令insmod acce加载驱动模块。
因为在程序中只为字符设备mdev申请了设备号,并为其创建设备节点,所以需要手动的进行创建设备节点(自动创建设备文件节点将在下一节中使用)。
(1)查询设备驱动模块和设备号
命令: cat /proc/devices
如上图信息可知,设备名为dev_module的主设备号为254,因为只有一个设备,所以次设备号为0。
(2)手动创建设备文件节点
命令:mknod /dev/dev_module c2540
命令节点字符设备 主设备号次设备号
创建成功后就可以在/dev目录下查询到设备文件节点了。
至此,设备节点创建成功,可以运行应用程序来操作设备驱动模块了。在此不讨论自动创建设备文件节点的话题。
2.用户空间实现
用户空间代码非常简单,首先是调用C库函数open打开设备文件节点/dev/dev_module,然后读取设备的数据,对照设备驱动代码,读取正确的数据应该是整型之1234,并将其打印出来。然后再将整型值4567通过write函数写入设备中。最后调用close关闭/dev/dev_module设备文件。
Makefile如下:
从Makefile可知,最后编译生成的用户空间的可执行文件名为app_access。
3.运行结果。
命令:./app_access
如上图可知,当用户空间程序运行时,先打开文件所以在内核空间调用dev_open函数,然后读取数据,在内核空间中调用dev_read函数,用户空间读取到数据之后,在用户空间打印读取到的数据“app--data = 1234”,即读取到的值为1234,余内核空间提供的值相同。然后再将整型之4567通过write函数写入到内核空间,在内核空间调用dev_write函数,接收到的值同样为4567;最后关闭设备节点文件。至此,说明通过用户空间操作内核空间的设备驱动模块成功。
二、Linux内核驱动的访问过程
如上图所示,对于设备驱动程序而言,用户空间的调用,可以在内核空间中找到与其相对应的操作函数方法。那么这些调用时如何进行一一对应的呢??整个调用的过程是怎样的??
在这里通过read调用进行示例说明。
1.从用户空间应用程序追踪
静态编译应用程序:
命令:arm-cortex_a8-linux-gnueabi-gcc -static -g acce -o app_access
应用程序编译生成可执行文件app_access,通过对其反汇编得到dump文件。
命令:arm-cortex_a8-linux-gnueabi-objdump -D -S app_access >dump
分析dump文件:
vim打开dump文件,查找到main函数,如下图:
如上图可知,各个调用所编译出的汇编代码,这里重点关注read。
从read(fd,&data,siaeof(data));调用可知,将数据保存在r0,r1和r2这三个寄存器中,从ARM汇编我们可知,C语言与汇编进行函数传参是,当参数小于等会4个时,参数保存在r0,r1,r2和r3寄存器中;所以read函数的3个参数分别按顺序保存在r0,r1和r2三个寄存器中。然后进行调用__libc_read函数。
通过在dump文件中可以查找到__libc_read函数,如下图:
在这里我们重点关注10917行和10918行,这两行代码。意思是,通过将立即数3存放在r7寄存器中,然后调用svc指令。Svc在ARM程序中是一个很特殊的指令,叫做系统调用指令。当调用svc指令后,PC指针(R15)会从用户空间跳转到内核空间,并且入口是统一固定的。
而此时内核空间所做的是获取一个number,即为以上存放在r7寄存器中的数字,对于read函数而言,这个数字为3,代表需要调用内核空间的哪一个系统调用函数,代表一个标号。最后根据这个namber数值进行查表,查找到对应的系统调用函数。
2.找调用的统一入口
其存在于Linux内核源码linux/arch/arm/kernel文件中,打开文件后,找到标号vector_swi,这就是用户空间到内核空间调用的统一入口。如下图:
通过注释”Get the system call number.”可知在这里也实现了查表操作。通过查找,找到名为sys_call_table的表,如下图:
最后还是在linux/arch/arm/kernel文件中查找到这张表,如下图:
如上图,定义.type sys_call_table, #object表之后,又调用ENTRY(sys_call_table)入口,然后包含calls.S文件。现在打开calls.S文件,其所在Linux内核源码路径为linux/arch/arm/kernel,如下图:
到这里,基本上是柳暗花明又一村了!如上图可见sys_read系统调用的定义,其标号刚好为3,所以sys_read就是与用户空间read调用对应的接口。其他的比如write、open等等调用均如此。
所以sys_call_table这个表实际上就是包好了很多系统调用函数的一个表,作用是从内核空间与用户空间的接口进行映射。
到这里基本上就很清楚了用户空间的read函数到内核空间的sys_read函数接口的过程了。
3.从sys_read系统调用接口道static struct file_operations操作方法函数集的过程
查找并打开linux文件,找到SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)宏定义,如下图:
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)宏定义展开后实际上就是sys_read函数的原型。
我们知道,当用户空间每打开一个文件,在Linux内核空间中都会存在一个struct file *file;结构指针;如上图中也存在一个文件描述符fd,然后通过方法函数调用file = fget_light(fd, &fput_needed);找对其文件描述符所对应的struct file *file;结构指针。最后利用struct file *file;去调用et = vfs_read(file, buf, count, &pos);。(我们知道,VFS虚拟文件系统对于Linux操作系统而言至关重要,它使得用户空间和内核空间分离独立,并且提供统一的接口进行交互)。继续跟踪vfs_read函数。
如上图所示,ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)函数原型中的ret = file->f_op->read(file, buf, count, pos);调用显得至关重要(以上代码的321行),实际上到这里,就是的整一个调用与Linux内核驱动中的static struct file_operations方法函数集扯上了关系。static struct file_operations方法函数实在Linux内核驱动中定义的设备操作方法,所以这就可从用户空间直接打通到Linux内核设备驱动上了。
三、总结
如下图为整个调用的框架图
本文为系列教程,查看其他文章,请关注本头条订阅号,谢谢!