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

简介: 上一篇文章讲了如何实现基于内核模块的“helloworld”,相信大家通过这个例子对于内核模块有了一个基本的了解。当然,内核模块绝不仅仅只能实现这点功能,其最大的应用就是实现硬件的驱动程序。其实,linux内核中很大一部代码都是硬件处理相关的,比如,设备-总线-驱动框架,USB框架、spi框架、i2c框架等等,对应于各种不同的硬件设备,相应的就会有设备驱动程序,从最简单的按键、LED驱动,到十分复杂的USB子系统驱动,可以好不夸张的说,Linux内核可以适配绝大多数的硬件设备。

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


相关文章
|
7月前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构
|
7月前
|
Linux 编译器 测试技术
探索Linux设备树:硬件描述与驱动程序的桥梁
探索Linux设备树:硬件描述与驱动程序的桥梁
619 0
|
7月前
|
Linux
Linux内核中USB设备驱动实现
Linux内核中USB设备驱动实现
104 0
|
7月前
|
存储 Linux Shell
udev用户空间设备管理
udev用户空间设备管理
161 0
|
存储 编译器 开发者
内核模块(下)
内核模块(下)
182 0
|
编译器 Linux 开发者
内核模块(上)
内核模块
115 0
|
存储 Linux 开发者
【Linux学习笔记】设备驱动模型详解——总线、设备、驱动和类
设备驱动是计算机系统中的重要组成部分,它们允许操作系统与硬件交互。设备驱动模型是一种通用的抽象框架,用于描述操作系统如何管理硬件设备。这里我们将介绍设备驱动模型中的四个关键概念:总线、设备、驱动和类。
|
Linux
linux总线设备驱动程序框架
linux总线设备驱动程序框架
156 0
|
数据采集 Shell 芯片
vxworks的pci设备驱动调试
vxworks的pci设备驱动调试
741 0
vxworks的pci设备驱动调试
|
Linux
Linux串口驱动程序(3)-打开设备
Linux串口驱动程序(3)-打开设备先来分析一下串口打开的过程: 1、用户调用open函数打开串口设备文件;2、在内核中通过tty子系统,把open操作层层传递到串口驱动程序中;3、在串口驱动程序中的xx_open最终实现这个操作。
1156 0