内核模块-实现一个简单的设备

713 阅读6分钟

上一篇文章讲了如何实现基于内核模块的“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除了申请了设备号之外,没有其他的功能。不过,我们已经有了设备文件,待后续增加文件相关的操作之后,就可以实现更复杂的功能。