一、Linux内核驱动的访问

1.内核空间实现

要促使用户空间的应用程序才能访问内核驱动模块,这么只能通过/dev目录下的设备节点进行访问。

linux设备驱动开发详解 源码_linux驱动开发项目_linux驱动源码分析

通过module_init(dev_init);申明,将staticint__initdev_init(void)函数作为此设备驱动的入口初始化函数,其完成(1)初始化字符设备cdev;(2)申请设备号,在这儿提供了两种申请的方法(静态申请和动态申请);(3)将字符设备mdev和设备号devno进行注册。注意:在这儿并没有创建设备文件。

字符设备mdev通用的可以进行静态申请和动态申请,在本程序Demo中使用了静态申请的方法,即直接定义全局变量structcdevmdev;。设备号通用也是通过全局变量devno储存,即为staticunsignedintdevno=0;。申请设备号的同时,也要为用户空间/dev目录下的设备节点进行命名,设备节点名为"dev_module"。

同样的,当设备驱动模块别卸载时,staticvoid__exitdev_exit(void)函数作为出口。

linux驱动开发项目_linux设备驱动开发详解 源码_linux驱动源码分析

通过module_exit(dev_exit);申明,将staticvoid__exitdev_exit(void)函数作为设备驱动模块卸载后的出口函数。其完成两个功能,将所申请到是字符设备mdev删掉,而且释放申请到的设备号devno。

在进行字符设备的注册时,使用的cdev_init函数其原型为:

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

从上图函数原型可知,须要传递参数有:字符设备cdev结构和所须要实现操作的方式函数集fops。在代码中字符设备结构为mdev,技巧函数集为dev_fops,定义如下:

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

如上图可知,成员owner的取值表示本设备模块所拥有。其他成员分别为dev_open实现打开设备,dev_read实现读取设备数据,dev_write实现写数据,dev_close实现关掉设备。她们分别对应staticstructfile_operations结构的相同属性成员技巧。具体方式实现如下:

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

linux驱动源码分析_linux驱动开发项目_linux设备驱动开发详解 源码

如上图所示,由于目前并未明晰的操作硬件设备,所以只是实现了相应方式的框架,之后通过复印的方法将调试信息输出进行调试。

当在用户空间的用户程序中调用open函数打开设备节点/dev/dev_module时,将会在内核空间中调用dev_open函数。当在用户空间中使用read函数操作/dev/dev_module设备节点文件时,同样的在内核空间中会调用dev_read函数执行操作。在staticssize_tdev_read(structfile*file,char__user*buf,size_tcount,loff_t*offset)函数中实现了当用户空间使用read函数访问时,将一个整型数据值1234传递回用户空间。数据传递的方式使用copy_to_user函数进行拷贝(从内核空间拷贝数据到用户空间)。

linux驱动源码分析_linux设备驱动开发详解 源码_linux驱动开发项目

如上图所示,实现了设备驱动的写操作和关掉操作。当在用户空间中调用write函数操作/dev/dev_module设备文件时linux行,这么对应的在内核空间执行dev_write函数调用。dev_write函数实现了将用户空间写入的时间读取下来,之后进行复印。当在用户空间调用close函数通用的将会在内核空间调用执行dev_close函数,进行关掉设备。

驱动程序完成后,通过编译生成文件access_driver.ko驱动文件。将其拷贝到板卡的根文件中,通过insmodaccess_driver.ko加载驱动模块。

由于在程序中只为字符设备mdev申请了设备号,并为其创建设备节点,所以须要自动的进行创建设备节点(手动创建设备文件节点将在下一节中使用)。

(1)查询设备驱动模块和设备号

:cat/proc/devices

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

如上图信息可知,设备名为dev_module的主设备号为254,由于只有一个设备,所以次设备号为0。

(2)自动创建设备文件节点

命令:mknod/dev/dev_modulec2540

命令节点字符设备主设备号次设备号

创建成功后就可以在/dev目录下查询到设备文件节点了。

至此,设备节点创建成功,可以运行应用程序来操作设备驱动模块了。在此不讨论手动创建设备文件节点的话题。

2.用户空间实现

linux设备驱动开发详解 源码_linux驱动开发项目_linux驱动源码分析

用户空间代码十分简单,首先是调用C库函数open打开设备文件节点/dev/dev_module,之后读取设备的数据,对照设备驱动代码,读取正确的数据应当是整型之1234,并将其复印下来。之后再将整型值4567通过write函数写入设备中。最后调用close关掉/dev/dev_module设备文件。

Makefile如下:

linux设备驱动开发详解 源码_linux驱动开发项目_linux驱动源码分析

从Makefile可知,最后编译生成的用户空间的可执行文件名为app_access。

3.运行结果。

命令:./app_access

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

如上图可知,当用户空间程序运行时,先打开文件所以在内核空间调用dev_open函数,之后读取数据,在内核空间中调用dev_read函数,用户空间读取到数据以后,在用户空间复印读取到的数据“app--data=1234”,即读取到的值为1234,余内核空间提供的值相同。之后再将整型之4567通过write函数写入到内核空间,在内核空间调用dev_write函数,接收到的值同样为4567;最后关掉设备节点文件。至此,说明通过用户空间操作内核空间的设备驱动模块成功。

二、Linux内核驱动的访问过程

linux驱动源码分析_linux驱动开发项目_linux设备驱动开发详解 源码

如上图所示,对于设备驱动程序而言,用户空间的调用,可以在内核空间中找到与其相对应的操作函数技巧。这么这种调用时怎样进行一一对应的呢??整个调用的过程是如何的??

在这儿通过read调用进行示例说明。

1.从用户空间应用程序追踪

静态编译应用程序:

命令:arm-cortex_a8-linux-gnueabi-gcc-static-gaccess_app.c-oapp_access

应用程序编译生成可执行文件app_access,通过对其反汇编得到dump文件。

命令:arm-cortex_a8-linux-gnueabi-objdump-D-Sapp_access>dump

linux驱动源码分析_linux驱动开发项目_linux设备驱动开发详解 源码

剖析dump文件:

vim打开dump文件,查找到main函数,如右图:

linux驱动源码分析_linux设备驱动开发详解 源码_linux驱动开发项目

如上图可知,各个调用所编译出的汇编代码,这儿重点关注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行linux嵌入式开发,这两行代码。意思是,通过将立刻数3储存在r7寄存器中linux设备驱动开发详解 源码,之后调用svc指令。Svc在ARM程序中是一个很特殊的指令,称作系统调用指令。当调用svc指令后,PC表针(R15)会从用户空间跳转到内核空间,而且入口是统一固定的。

而此时内核空间所做的是获取一个number,即为以上储存在r7寄存器中的数字,对于read函数而言,这个数字为3,代表须要调用内核空间的哪一个系统调用函数,代表一个标号。最后依照这个namber数值进行查表,查找到对应的系统调用函数。

2.找调用的统一入口

其存在于Linux内核源码linux/arch/arm/kernel/entry-common.S文件中,打开文件后,找到标号vector_swi,这就是用户空间到内核空间调用的统一入口。如右图:

linux设备驱动开发详解 源码_linux驱动开发项目_linux驱动源码分析

通过注释”Getthesystemcallnumber.”可知在这儿也实现了查表操作。通过查找,找到名为sys_call_table的表linux设备驱动开发详解 源码,如右图:

最后还是在linux/arch/arm/kernel/entry-common.S文件中查找到这张表,如右图:

linux驱动开发项目_linux驱动源码分析_linux设备驱动开发详解 源码

如上图,定义.typesys_call_table,#object表以后,又调用ENTRY(sys_call_table)入口,之后包含calls.S文件。现今打开calls.S文件,其所在Linux内核源码路径为linux/arch/arm/kernel/calls.S,如右图:

linux设备驱动开发详解 源码_linux驱动源码分析_linux驱动开发项目

到这儿,基本上是柳暗花明又一村了!如上图可见sys_read系统调用的定义,其标号正好为3,所以sys_read就是与用户空间read调用对应的插口。其他的例如write、open等等调用均这么。

所以sys_call_table这个表实际上就是包好了好多系统调用函数的一个表,作用是从内核空间与用户空间的插口进行映射。

到这儿基本上就很清楚了用户空间的read函数到内核空间的sys_read函数插口的过程了。

3.从sys_read系统调用插口道staticstructfile_operations操作方式函数集的过程

查找并打开linux/fs/read_write.c文件,找到SYSCALL_DEFINE3(read,unsignedint,fd,char__user*,buf,size_t,count)宏定义,如右图:

linux驱动开发项目_linux设备驱动开发详解 源码_linux驱动源码分析

SYSCALL_DEFINE3(read,unsignedint,fd,char__user*,buf,size_t,count)宏定义展开后实际上就是sys_read函数的原型。

我们晓得,当用户空间每打开一个文件,在Linux内核空间中还会存在一个structfile*file;结构表针;如上图中也存在一个文件描述符fd,之后通过方式函数调用file=fget_light(fd,&fput_needed);找对其文件描述符所对应的structfile*file;结构表针。最后借助structfile*file;去调用et=vfs_read(file,buf,count,&pos);。(我们晓得,VFS虚拟文件系统对于Linux操作系统而言至关重要,它促使用户空间和内核空间分离独立,但是提供统一的插口进行交互)。继续跟踪vfs_read函数。

linux驱动源码分析_linux设备驱动开发详解 源码_linux驱动开发项目

如上图所示,ssize_tvfs_read(structfile*file,char__user*buf,size_tcount,loff_t*pos)函数原型中的ret=file->f_op->read(file,buf,count,pos);调用变得至关重要(以上代码的321行),实际上到这儿,就是的整一个调用与Linux内核驱动中的staticstructfile_operations方式函数集扯上了关系。staticstructfile_operations方式函数实在Linux内核驱动中定义的设备操作方式,所以这就可从用户空间直接打通到Linux内核设备驱动上了。

三、总结

如右图为整个调用的框架图

linux驱动开发项目_linux驱动源码分析_linux设备驱动开发详解 源码

本文原创地址://q13zd.cn/lnhqdmkxxjxz.html编辑:刘遄,审核员:暂无