上一篇文章讲了如何实现基于内核模块的“helloworld”,相信大家通过这个例子对于内核模块有了一个基本的了解。当然,内核模块绝不仅仅只能实现这点功能,其最大的应用就是实现硬件的驱动程序。其实,linux内核中很大一部代码都是硬件处理相关的,比如,设备-总线-驱动框架,USB框架、spi框架、i2c框架等等,对应于各种不同的硬件设备,相应的就会有设备驱动程序,从最简单的按键、LED驱动,到十分复杂的USB子系统驱动,可以好不夸张的说,Linux内核可以适配绝大多数的硬件设备。
那这些驱动框架和驱动程序,一般都是通过内核模块的方式设计和使用的。在构建内核时,一般将设备驱动程序编译成内核模块,内核可以根据系统接入的硬件情况,动态的加载、卸载相应的驱动模块。
那具体到一个硬件驱动程序,是如何与内核模块结合在一起的呢?下面通过一个简单的字符设备驱动例子说明一下。
设备存在方式--设备文件
“一切皆文件”是Linux的十分重要的设计哲学。内核为应用程序提供的所有服务都是通过“文件”的形式。设备也不例外,任何硬件最终都会在文件系统中创建一个对应的文件,可以通过命令ls /dev看到系统中所有的设备文件。例如,大家熟悉的鼠标,其设备文件的主要信息如下所示:
ls input/mouse0 -l crw-rw---- 1 root input 13, 32 5月 16 22:42 input/mouse0
其中,c代表设备类型为字符设备,rw-rw----表示用户对于文件的操作权限,13,52表示设备的主、次设备号,这个下一节会着重点讲解。
用户空间的应用程序,通过这些设备文件,就可以完成与硬件设备的交互。操作设备文件的方式与操作普通文件没有任何区别,都是通过标准的文件操作接口完成。
- 打开设备:open
- 关闭设备:close
- 写设备参数:write
- 读设备参数:read
- 控制设备:ioctl
- ... ...
在“一切皆文件”这种哲学的指导下,Linux系统的设备管理十分的和谐、统一,只要你学会了操作普通文件,那么操作任何设备都没有太大的问题,至少你可以不需要太多学习,就可以完成对一个设备的使用。
设备标识--设备号
Linux系统会使用很多的硬件设备,去/dev目录下看一下就知道了。那这么多的设备文件,内核是如何进行区分的呢?其实很简单,就是通过一个数字名字进行区分的,这个数字就是设备号。不同的设备文件拥有系统唯一的设备号,内核通过这个设备号完成设备的识别。
设备号,分为两部分:主设备号和次设备号。主设备号用来定义设备的类型,次设备用来定义同属于某一类型的设备编号。这就好比,在学校里,会给每个班级编号,具体每个班级的里学生,又会通过学号进行编号,对应一下,主设备号就是班级编号,次设备号就是班内学生编号。
通常而言,一个驱动程序会对应唯一的一个主设备号,而每个被其驱动的硬件设备,对应一个次设备号。
内核通过dev_t表示设备号,其包括主、次设备号两部分。一般不会通过dev_t直接解析主次设备号,而是通过下面的宏进行操作:
- MAJOR(dev_t dev);获取主设备号 - MINOR(dev_t dev);获取次设备号 - MKDEV(int major, int minor);根据主次设备号合成设dev_t类型
申请和释放设备号
不同类型的设备,管理设备号的方式是不同的,由于本文举的驱动例子是字符类型的,所以只介绍一下,字符类设备的设备号的申请和释放方式。
设备编号的申请有两种方式:
- 已经主设备号:
如果事先,已经知道了主设备,那么可以使用接口申请设备号。
int register_chdev_region(dev_t first, unsigned int count, char*name); - first:主设备号 - count:连续的次设备的个数,次设备号一般从0开始 - name:设备名称
- 未知主设备号:
如果事先,不知道主设备号,那么使用下面的接口,内核会动态的分配一个可用的主设备供该设备使用。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); - dev:保存内核分配的设备号 - firstminor:起始次设备号 - count:连续的次设备号个数 - name:设备名称
不论使用哪种方式,设备号申请成功,返回0,失败返回错误码。
设备号是系统资源,不使用时应该主动的将其释放,该操作一般发生在驱动卸载时,使用的接口定义如下。
void unregister_chdev_region(dev_t first, unsigned int count);
可以通过查看/proc/devices,来看系统中硬件设备文件的设备号。
cat devices Character devices: 1 mem 4 /dev/vc/0 ... ... Block devices: 7 loop 8 sd 9 md ... ...
简单字符设备
好了,有了设备号的概念之后, 就可以在内核模块中添加申请设备号的操作,而后就可以在/dev目录下,看到设备文件。这一个真正意义的字符设备文件,下面我们就来实现一个这个设备。
上一小节说过,设备号可以静态指定,也可以动态生成,我们采用两种方式来申请设备编号。通过定义一个全局的变量:global_major,来保存设备号,如果global_major为0,那么采用动态设备号申请方式,否则,采用global_major来申请设备号。
//scdev.c #include <linux/fs.h> #include <linux/init.h> #include <linux/module.h> static int global_major = 0; static int global_minor = 1; static int global_nr_devs = 2; static int __init module_init_func(void) { int ret; dev_t dev; printk("register simple cdev.\n"); if(global_major) { dev = MKDEV(global_major, global_minor); ret = register_chrdev_region(dev, global_nr_devs, "scdev"); } else { ret = alloc_chrdev_region(&dev, global_major, global_nr_devs, "scdev"); global_major = MAJOR(dev); global_minor = MINOR(dev); } if(ret < 0) { printk(KERN_WARNING "scdev:can't get major %d.\n", global_major); return ret; } printk(KERN_INFO "scdev:major:%d, minor:%d.\n", global_major, global_minor); return 0; } static void __exit module_exit_func(void) { return; } MODULE_LICENSE("GPL v2"); MODULE_VERSION("v0.1"); MODULE_AUTHOR("lhl"); MODULE_DESCRIPTION("LKM, scdev."); module_init(module_init_func); module_exit(module_exit_func);
Makefile文件
obj-m:=scdev.o KERS :=/lib/modules/$(shell uname -r)/build all: make -C $(KERS) M=$(shell pwd) modules clean: make -C $(KERS) M=$(shell pwd) clean
编译成功后, 通过sudo install scdev.ko,安装模块,通过dmesg可以看到动态申请的设备号:
[10169.420211] register simple cdev. [10169.420212] scdev:major:239, minor:0.
动态申请的次设备号从0开始,可是,这个设备还没有生成设备文件,通过mknod命令,就可以创建设备文件。
sudo mknod /dev/scdev c 239 0 ls /dev/scdev -l crw-r--r-- 1 root root 239, 0 5月 18 21:26 /dev/scdev
不过,由于没有实现文件相关的操作,所以,如果试图往读取或者写入数据到scdev时,系统会提示:
cat /dev/scdev cat: /dev/scdev: 没有那个设备或地址 或 echo 0 > /dev/scdev bash: /dev/scdev: 没有那个设备或地址
后续实现文件相关操作之后,就可以实现设备文件的读取和写入。
总结
本文主要介绍了,如何基于设备模块实现一个简单的字符设备scdev,并且创建了相应的设备文件。但是,这个scdev除了申请了设备号之外,没有其他的功能。不过,我们已经有了设备文件,待后续增加文件相关的操作之后,就可以实现更复杂的功能。