Linux内核设计与实现第十周读书笔记

时间:2023-12-19 18:15:02

第十七章 设备与模块

关于设备驱动与设备管理,我们讨论四种内核成分。

  • 设备类型
  • 模块
  • 内核对象
  • sysfs

17.1设备类型

在Linux以及所有Unix系统中,设备被分为以下三种类型:

  • 块设备,块设备通常缩写为blkdev,它是可寻址的,寻址以块为单位,块大小随设备不同而不同;块设备通常支持重定位操作,也就是对数据的随机访问。块设备是通过称为“块设备节点”的特殊文件来访问,并且通常被挂载为文件系统。
  • 字符设备,字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,就是一个一个字符,或者一个一个字节。字符设备通过称为“字符设备节点”的特殊文件访问。与块设备不同,应用程序直接通过访问设备节点与字符设备交互。
  • 网络设备最常见的类型有事也以以太网设备来称呼,它提供了对网络的访问,这是通过一个物理适配器和一种特定的协议来进行的。它不通过设备节点来访问,而是通过套接字API这样的特殊接口来访问。

并不是所有设备驱动都表示物理设备。有些设备驱动是虚拟的,仅提供访问内核功能而已。我们成为“伪设备”。

17.2模块

支持模块的好处是基本内核镜像可以尽可能地小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。

17.2.1Hello,World

MOUDLE_LICENSE()宏用于指定模块的版权。

MOUDLE_AUTHOR()宏和MOUDLE_DESCRIPTION()宏指定了代码作者和模块的简要描述,它们完全是用作信息记录目的。

17.2.2构建模块

1.放在内核源代码树中

  • 你需要在drivers/char/目录下建立一个名为fishing的子目录。
  • 接下来需要在drivers/char/的Makefile文件中添加一行。编辑drivers/char/Makefile/并加入obj-m += fishing/。该行编译指令告诉模块构建系统,在编译模块时需要进入fishing/子目录中。更可能发生的情况是驱动程序的编译取决于一个特殊配置选项,比如可能的CONFIG_FISHING_POLE,如果这样,需要用下面的指令替换刚才的指令:obj-$(CONFIG_FISHING_POLE)  +=fishing/
  • 最后,在drivers/char/fishing/下,需要添加一个新的Makefile文件,其中需要有这条指令:obj-m += fishing.o

2.放在内核代码外

  • 在你自己的源代码树目录中建立一个Makefile文件,只需要一行指令:obj-m := fishing.o
  • 告诉make如何找到内核源代码文件和基础的Makefile文件。make –C /kernel/source/location SUBDIRS=$PWD modules

17.2.3安装模块

下面的构建命令用来安装编译的模块到合适的目录下:make moudles_install,通常需要以root权限运行。

17.2.4产生模块依赖性

  • 若想产生内核依赖关系的信息,root用户可运行命令depmod
  • 为了执行更快的更新操作,那么可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这是root用户可运行命令的depmod -A

17.2.5载入模块

  • 载入模块,以root身份运行命令:insmod module.ko
  • 卸载一个模块,以root身份运行:rmmod module
  • 为了在内核via modprobe中插入模块,需要以root身份运行:modprobe module[module parameters]
  • modprobe命令也可用来从内核中卸载模块,当然这也需要以root身份运行:modprobe -r modules

与rmmod命令不同,modprobe也会卸载给定模块所依赖的相关模块,但其前提是这些相关模块没有被使用。

17.2.6管理配置选项

如果你建立了一个新子目录,而且也希望kconfig文件存在于该目录中的话,那么你必须在一个已存在的kconfig文件中将它引入。你需要加入下面一行指令:source "drivers/char/fishing/Kconfig"

17.2.7模块参数

定义一个模块参数可通过宏module_param()完成:module_param(name,type,perm);

参数name既是用户可见的参数名,也是你模块中存放模块参数的变量名。参数type则存放了参数的类型。最后一个参数perm指定了模块在sysfs文件系统下对应文件的权限。

17.2.8导出符号表

在内核中,导出内核函数需要使用特殊的指令:EXPORT_SYMBOL()EXPORT_SYMBOL_GPL()

导出的内核函数可以被模块调用。导出的内核符号表被看作导出的内核接口,甚至称为内核API。

17.3设备模型

17.3.1kobject

设备模型的核心部分是kobject(kernel object)kobject提供了诸如引用计数、名称和父指针等字段,可以创建对象的层次结构。

name指针指向kobject的名称。

parent指针指向kobject的父对象。

kref提供引用计数,ktype和kset结构体对kobject对象进行描述和分类。

struct kobject {
       constchar              *name;
       structlist_head       entry;
       structkobject         *parent;
       structkset              *kset;
       structkobj_type     *ktype;
       structsysfs_dirent   *sd;
       structkref              kref;
       unsignedint state_initialized:1;
       unsignedint state_in_sysfs:1;
       unsignedint state_add_uevent_sent:1;
       unsignedint state_remove_uevent_sent:1;
       unsignedint uevent_suppress:1;
};

17.3.2ktype

struct kobj_type {
       void(*release)(struct kobject *kobj);
       structsysfs_ops *sysfs_ops;
       structattribute **default_attrs;
};

ktype是为了描述kobject所具有的普遍特性。

release指针指向在kobject引用计数减至0时要被调用的析构函数。该函数负责释放所有kobject使用的内存和其他相关清理工作。

sysfs_ops变量指向sysfs_ops结构体。该结构体描述了sysfs文件读写时的特性。

default_attrs指向一个attribute结构体数组。这些结构体定义了该kobject相关的默认属性。

17.3.3kset

kset是kobject对象的集合体。kset可把kobject集中到一个集合中,而ktype描述相关类型kobject所共有的特性。区别:具有相同ktype的kobject可以被分组到不同的kset。就是说在Linux内核中只有少数一些的ktype,却有多个kset。

struct kset {
       structlist_head list;
       spinlock_tlist_lock;
       structkobject kobj;
       structkset_uevent_ops *uevent_ops;
};

list连接kset中所有的kobject对象,list_lock是保护这个链表元素的自旋锁,kobj指向的kobject对象代表了该集合的基类。uevent_ops指向一个结构体用于处理集合中kobject对象的热插拔操作。

17.3.4kobject、ktype和kset的相互关系

kobject本身的意义不大,通常情况下需要被嵌入到其他数据结构中,让那些包含它的结构具有了kobject的特性。

kobject与一个特别的ktype对象关联,ktype定义了一些kobject相关的默认特性:析构行为、sysfs行为以及别的一些默认属性。

kobject又归入了称作kset的集合。kset提供了两个功能:1、其中嵌入的kobject作为kobject组的基类2、kset将相关的kobject集合在一起。

17.3.5管理和操作kobject

  • 使用kobject的第一步需要先来生命和初始化。kobject通过函数kobiect_init进行初始化。
  • 该函数的第一个参数就是需要初始化的kobject对象,在调用初始化函数前,kobject必须清空。
  • 在清零后,就可以安全的初始化parent和kset字段。
  • 这多步操作也可以由kobject_create()来自动处理,它返回一个新分配的kobject。

17.3.6引用计数

kobject的主要功能之一就是为我们提供了一个统一的引用计数系统。

1.递增和递减引用计数

  • 增加一个引用计数可通过kobject_get()函数完成:struct kobject * kobject_get(struct kobject *kobj);
  • 减少引用计数通过kobject_put()完成:void kobject_put(struct kobject *kobj);

2.kref

struct kref{
atomic_t refcount;
};

其中唯一的字段是用来存放引用计数的原子变量。那么为什么采用结构体?这是为了便于进行类型检测。

17.4sysfs

sys文件系统是一个处于内存中的虚拟文件系统,它为我们提供了kobject对象层次结构的视图。

17.4.1sysfs中添加和删除kobject

想要把kobject导入sysfs,需要用到函数kobject_add ()int kobject_add(struct kobject *kobj,struct kobject *parent,const char *fmt, ...);

kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。

从sysfs中删除一个kobject对应文件目录,需要使用函数kobject_del()

void kobject_del(struct kobject *kobj);

17.4.2向sysfs中添加文件

1.默认属性

默认的文件集合是通过kobject和kset中的ktype字段提供的。因此所有具有相同类型的kobject在它们对应的sysfs目录下都拥有相同的默认文件集合。这些属性负责将内核数据映射成sysfs中的文件。

2.创建新属性

内核为能在默认集合之上,再添加新属性而提供了sysfs_create_file()接口:int sysfs_cerate_file(struct kobject *kobj,const struct attribute *attr);

除了添加文件外,还有可能需要创建符号连接。在sysfs中创建一个符号连接相当简单:int sysfs_create_link(struct kobject *kobj,struct kobject *target,char *name);

3.删除属性

删除一个属性需通过函数sysfs_remove_file():void sysfs_remove_file(struct kobject *kobj,const struct attribute *attr);

sysfs_creat_link()创建的符号连接可通过函数sysfs_remove_link()删除:void sysfs_remove_link(struct kobject *kobj,char *name);

4.sysfs约定

  • 首先,sysfs属性应该保证每个文件只导出一个值,该值应该是文本形式而且映射为简单C类型。其目的是为了避免数据的过度结构化或太凌乱。
  • 其次,在sysfs中要以一个清晰的层次组织数据。
  • 最后,记住sysfs提供内核到用户空间的服务,这多少有些用户空间ABI(应用程序二进制接口)的作用。

17.4.3内核事件层

  • 内核事件层实现了内核到用户的消息通知系统——就是建立在上文一直讨论的kobject基础之上。
  • 每个事件都被赋予了一个动词或动作字符串表示信号。该字符串会以“被修改过”或“未挂载”等词语来描述事件。
  • 最后,每个事件都会有一个可选的负载。
  • 使用kobject和属性不但有利于很好的实现基于sysfs的事件,同时也有利于创建新kobjects对象和属性来表示新对象和数据——它们尚未出现在sysfs中。