0%

《趣谈linux操作系统》小结(二十七) - 字符设备驱动

字符设备驱动

鼠标和打印机都是字符设备,对应的驱动代码分别在drivers/input/mouse/logibm.c、drivers/char/lp.c文件中。

1
2
3
4
5
6
module_init(logibm_init);
module_exit(logibm_exit);


module_init(lp_init_module);
module_exit(lp_cleanup_module);

内核模块

驱动作为一个内核模块, 通过使用insmod加载到内核, 可以使用命令lsmod查看已经加载的内核。

图片替换文本

构建一个内核模块需要几个部分:

第一、头文件部分

一般内核需要包含2个头文件,当然也可以包含更多其他头文件

1
2
3

#include <linux/module.h>
#include <linux/init.h>

第二、定义一些函数,用于处理内核模块的主要逻辑。

例如打开、关闭、读取、写入设备的函数或者响应中断的函数。

第三部分,定义一个 file_operations 结构

设备要想被文件系统的接口操作,也需要类似的接口。

第四部分,定义整个模块的初始化函数和退出函数

用于加载和卸载这个 ko 的时候调用。

第五部分,调用 module_init 和 module_exit

分别指向上面两个初始化函数和退出函数。

第六部分,声明一下 lisense,调用 MODULE_LICENSE。

有了以上六个部分,模块就可以工作了。

打开字符设备

图片替换文本

使用字符设备,首先要写一个内核模块,然后通过insmod加载该模块, 加载模块会调用module_init调用的初始化函数。例如,在 lp.c 的初始化函数 lp_init 对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

static int __init lp_init (void)
{
......
if (register_chrdev (LP_MAJOR, "lp", &lp_fops)) {
printk (KERN_ERR "lp: unable to get major %d\n", LP_MAJOR);
return -EIO;
}
......
}


int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
......
cd = __register_chrdev_region(major, baseminor, count, name);
cdev = cdev_alloc();
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
cd->cdev = cdev;
return major ? 0 : cd->major;
}

字符设备驱动的内核模块加载的时候,首先会注册这个字符设备。调用 __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
2
3
4
5
6
7
8
9
10

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
int error;
struct fd f = fdget(fd);
......
error = do_vfs_ioctl(f.file, fd, cmd, arg);
fdput(f);
return error;
}

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,沟通交流。