字符设备驱动
鼠标和打印机都是字符设备,对应的驱动代码分别在drivers/input/mouse/logibm.c、drivers/char/lp.c文件中。
1 | module_init(logibm_init); |
内核模块
驱动作为一个内核模块, 通过使用insmod加载到内核, 可以使用命令lsmod查看已经加载的内核。
构建一个内核模块需要几个部分:
第一、头文件部分
一般内核需要包含2个头文件,当然也可以包含更多其他头文件
1 |
第二、定义一些函数,用于处理内核模块的主要逻辑。
例如打开、关闭、读取、写入设备的函数或者响应中断的函数。
第三部分,定义一个 file_operations 结构
设备要想被文件系统的接口操作,也需要类似的接口。
第四部分,定义整个模块的初始化函数和退出函数
用于加载和卸载这个 ko 的时候调用。
第五部分,调用 module_init 和 module_exit
分别指向上面两个初始化函数和退出函数。
第六部分,声明一下 lisense,调用 MODULE_LICENSE。
有了以上六个部分,模块就可以工作了。
打开字符设备
使用字符设备,首先要写一个内核模块,然后通过insmod加载该模块, 加载模块会调用module_init调用的初始化函数。例如,在 lp.c 的初始化函数 lp_init 对应的代码如下
1 |
|
字符设备驱动的内核模块加载的时候,首先会注册这个字符设备。调用 __register_chrdev_region,注册字符设备的主次设备号(在major.h定义好了)和名称,然后分配一个 struct cdev 结构,将 cdev 的 ops 成员变量指向这个模块声明的 file_operations。然后,cdev_add 会将这个字符设备添加到内核中一个叫作 struct kobj_map *cdev_map 的结构,来统一管理所有字符设备。
其中,MKDEV(cd->major, baseminor) 表示将主设备号和次设备号生成一个 dev_t 的整数,然后将这个整数 dev_t 和 cdev 关联起来。
这里对于鼠标这种输入设备,又被封装了一层, 通过input.c来管理。
内核模块加载完毕后,接下来要通过 mknod 在 /dev 下面创建一个设备文件,只有有了这个设备文件,我们才能通过文件系统的接口,对这个设备文件进行操作。mknod会让设备和这个文件关联起来, 创建一个inode节点,保存文件的操作最终调用的是设备的操作。打开的话, 从文件系统的open, 一直到调用驱动的open,打开设备。
写入字符设备
对于文件的读写操作也和文件的读写操作一样,只是最终调用到的是驱动的读写函数。
使用 IOCTL 控制设备
对于 I/O 设备来讲,我们前面也说过,除了读写设备,还会调用 ioctl,做一些特殊的 I/O 操作。
1 |
|
ioctl也是一个系统调用。
其中,fd 是这个设备的文件描述符,cmd 是传给这个设备的命令,arg 是命令的参数。其中,对于命令和命令的参数,使用 ioctl 系统调用的用户和驱动程序的开发人员约定好行为即可。
其实 cmd 看起来是一个 int,其实他的组成比较复杂,它由几部分组成:最低八位为 NR,是命令号;然后八位是 TYPE,是类型;然后十四位是参数的大小;最高两位是 DIR,是方向,表示写入、读出,还是读写。这里了解一下就ok了, 用的时候可以查一下文档。
ioctl 中会调用 do_vfs_ioctl,这里面对于已经定义好的 cmd,进行相应的处理。如果不是默认定义好的 cmd,则执行默认操作。对于普通文件,调用 file_ioctl;对于其他文件调用 vfs_ioctl。
调用的是 struct file 里 file_operations 的 unlocked_ioctl 函数。我们前面初始化设备驱动的时候,已经将 file_operations 指向设备驱动的 file_operations 了。这里调用的是设备驱动的 unlocked_ioctl。对于打印机程序来讲,调用的是 lp_ioctl。
好了, 这里也终于了解了ioctl这个系统调用了,以前和驱动联调的时候,也都是通过这个函数来调用的。 理顺了。
内核模块编译
linux内核使用的是kbuild编译系统,在编译可加载模块时,其makefile的风格和常用的编译C程序的makefile有所不同,尽管如此,makefile的作用总归是给编译器提供编译信息。
网上看到了一个相关的文章, 后续有用再查查
今天最后一天上班了, 过两天就回去过年了,春节快乐!~~~
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。