【读书笔记】Linux内核设计与实现--设备与模块

文章目录

  • 1.设备类型
  • 2.模块
    • 2.1 Hello, World
    • 2.2 构建模块
    • 2.3 安装模块--make modules_install(需要root权限)
    • 2.4 产生模块依赖性--depmod
    • 2.5 载入模块--insmod/rmmod/modprobe
    • 2.6 管理配置选项--config选项
    • 2.7 模块参数--module_param()
    • 2.8 导出符号表--EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()
  • 3.设备模型
    • 3.1 kobject--设备模型的核心部分
    • 3.2 ktype--kobject对象被关联到一种特殊类型
    • 3.3 kset--kobject对象的集合体
    • 3.4 kobject、ktype和kset的相互关系--内部相互交织
    • 3.5 管理和操作kobject
    • 3.6 引用计数--kobject的主要功能之一
  • 4.sysfs--处于内存中的虚拟文件系统
    • 4.1 sysfs中添加和删除kobject--kobject_add()/kobject_del()
    • 4.2 向sysfs中添加文件
    • 4.3 内核事件层--建立在kobject基础上的内核到用户的消息通知系统

关于设备驱动和设备管理,内核中存在四种成分:

  1. 设备类型:在所有Unix系统中为了统一普通设备的操作所采用的分类;
  2. 模块:Linux内核中用于按需加载和卸载目标码的机制;
  3. 内核对象:内核数据结构中支持面向对象的简单操作,还支持维护对象之间的父子关系;
  4. sysfs:表示系统中设备树的一个文件系统。

1.设备类型

设备类型 描述
块设备 缩写为blkdev,可寻址,以块为单位,块大小随设备不同而不同;
支持重定位操作(seeking,随机访问);
通过称为“块设备节点”的特殊文件进行访问,通常被挂载为文件系统。
字符设备 缩写为cdev,不可寻址数据流式访问
通过称为“字符设备节点”的特殊文件进行访问。
应用程序通过直接访问设备节点与字符设备交互。
网络设备 有时以以太网(ethernet devices)来称呼,提供了对网络的访问,
通过一个物理适配器(网卡)和一种特定的协议(ip协议)进行。
网络设备打破了Unix的“所有文件都是文件”的设计原则,不是通过设备节点来访问,
而是通过套接字API的特殊接口进行访问
杂项设备
(miscellaneous device)
简写为miscdev,实际上是个简化的字符设备;杂项设备使驱动程序开发很容易表示一个简单设备–实际上使对通用基本架构的一种折中。
伪设备
(pseudo device)
虚拟的设备驱动,仅提供访问内核功能,如内核随机数发生器(/dev/random和/dev/urandom)、空设备(/dev/null)、零设备(/dev/zero)、满设备(/dev/full)、内存设备(/dev/mem)。

2.模块

Q:何为模块?
A:Linux是模块化的,它允许内核在运行时动态地向其中插入或从中删除代码(相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,简称模块

支持模块的好处是基本内核镜像可以仅可能的小,因为可选的功能和驱动程序可以利用模块形式再提供。

2.1 Hello, World

如果驱动文件被静态地编译到内核映像中,那么退出函数将不被包含,而且永远都不会被调用。

MODULE_AUTHOR()宏和MODULE_DESCRIPTION()宏指定了代码作者和模块的简要描述,用作信息记录目的,无他目的。

2.2 构建模块

在2.6内核中,采用了新的”kbuild“构建系统,构建过程的第一步是决定在哪里管理模块源码,方式如下:

  1. 把模块源码加入到内核源代码树种;
  2. 在内核源代码树之外维护和构建模块源码。

2.3 安装模块–make modules_install(需要root权限)

2.4 产生模块依赖性–depmod

2.5 载入模块–insmod/rmmod/modprobe

2.6 管理配置选项–config选项

2.7 模块参数–module_param()

Linux提供了这样一个简单框架–它可允许驱动程序声明参数,从而用户可以在系统启动或者模块装载时再指定参数值,这些参数对于驱动而言属于全局变量。

格式如下:

module_param(name, type, perm);

参数name是用户可见的参数名,也是模块中存放模块参数的变量名。
参数type存放参数的类型。(byte、short、int、ushort、uint、long、ulong、bool或invbool)。
参数perm指定了模块在sysfs文件系统下对于文件的权限,该值可以是八进制格式(0644),也可以是S_Ifoo的定义形式(S_IRUGO | S_IWUSR),如果该值是零,则表示禁止所有的sysfs项。

ps:模块参数同时也将会出现在sysfs文件系统种。

2.8 导出符号表–EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()

模块被载入后,就会动态地链接到内核,只有被显示导出后的外部函数,才可以被外部调用。
导出的内核函数可以被模块调用,而未导出的函数模块则无法被调用
被导出的符号表所含的函数必然也要是非静态的。

导出的内核符号表被看作导出的内核接口(也可称为内核API),使用如下:

int getvalue()
{
	return vaule;
}
EXPORT_SYMBOL(getvalue);

ps:
有些接口仅仅希望对GPL兼容的模块可见,内核连接器使用MODULE_LICENSE()宏可满足该要求 ;如果希望函数仅仅对标记为GPL协议的模块可见,EXPORT_SYMBOL_GPL()可满足该要求。

3.设备模型

统一设备模型(device model)是2.6内核增加的一个引人注目的新特性。
设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使系统具有以下优点:

  1. 代码重复最小化;
  2. 提供统一机制,如引用计数;
  3. 可以列举系统中所有的设备,观察状态,并查看其所连接的总线;
  4. 可以将系统中的全部设备结构以树的形式完整、有效地展现出来–包括所有的总线和内部连接;
  5. 可以将设备和其对应的驱动联系起来,反之亦然;
  6. 可以将设备按照类型加以归类,比如分类为输入设备,而无需理解物理设备的拓扑结构;
  7. 可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源

3.1 kobject–设备模型的核心部分

设备模型的核心部分就是kobject(kernel object),由struct kobject结构体表示,定义于头文件中。
kobject类似于C#或java这些面向对象语言中的对象(object)类,提供了诸如引用计数、名称和父指针等字段,可以创建对象的层次结构。

struct kobject {
	const char		*name;							/* 指向此kobject的名称 */
	struct list_head	entry;
	struct kobject		*parent;					/* 指向kobject的父对象,sysfs的真实面目:一个用户空间的文件系统,用来表示内核中kobject对象的层次结构 */
	struct kset		*kset;
	struct kobj_type	*ktype;
	struct sysfs_dirent	*sd;						/* 指向sysfs_dirent结构体,该结构体在sysfs中表示的就是这个kobject */
	struct kref		kref;							/* 提供引用计数 */
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};

kobject通常是嵌入其他结构中,如定义于中struct cdev中才是真正需要用到kobject结构:

/*cdev struct 表示该对象代表一个字符设备*/
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

当kobject被嵌入到其他结构中时,该结构拥有了kobject提供的标准功能。
嵌入kobject的结构体可以成为对象层次架构中的一部分。

3.2 ktype–kobject对象被关联到一种特殊类型

ktype(kernel object type)是kobject对象被关联到一种特殊的类型。
ktype由kobj_type结构体表示,定义于头文件中:

struct kobj_type {
	void (*release)(struct kobject *kobj);	/* release 指针指向在kobject引用计数减至零时要被调用的析构函数,该函数负责释放所有kobject使用的内存和其他相关清理工作 */
	const struct sysfs_ops *sysfs_ops;		/* sysfs_ops变量指向sysfs_ops结构体,该结构体描述了sysfs文件读写时的特性 */
	struct attribute **default_attrs;		/* 指向一个attribute结构体数组,定义了该kobject相关的默认属性,属性描述了给定对象的特征,该数组的最后一项必须为NULL */
	const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
	const void *(*namespace)(struct kobject *kobj);
};

Q:ktype存在的意义?
A:ktype的存在是为了描述一族kobject所具有的普遍特性。这样不再需要每个kobject都分别定义自己的特性,而是将这些普遍的特性在ktype结构体中一次定义,然后所有“同类”的kobject都能共享一样的特性。

3.3 kset–kobject对象的集合体

kset可把kobject集中到一个集合中,而ktype描述相关类型kobject所共有的特性,它们之间的重要区别在于:具有相同ktype的kobject可以被分组到不同的kset

kobject的kset指针指向相应的kset集合。
kset集合由kset结构体表示,定义于头文件中:

struct kset {
	struct list_head list;			/* list连接该集合(kset)中所有的kobject对象 */
	spinlock_t list_lock;			/* list_lock是保护这个链表中元素的自旋锁 */
	struct kobject kobj;			/* kobj指向的kobject对象代表了该集合的基类 */
	const struct kset_uevent_ops *uevent_ops;	/* 指向一个结构体,用于处理集合中kobject对象的热插拔操作,uevent就是用户事件(user event),提供了与用户空间热插拔信息进行通信的机制 */
};

3.4 kobject、ktype和kset的相互关系–内部相互交织

kobject:由struct kobject表示,引入诸如引用计数、父子关系和对象名称等基本对象,以一个统一的方式提供这些功能。通常需要被嵌入到其他数据结构中,给与那些包含该它的结构具有kobject的特性。

ktype:kobject对象被关联到一种特殊类型即是ktype(实际也是kobject)。ktype由struct kobj_type结构体表示,在kobject中ktype字段指向该对象。ktype定义了一些kobject相关的默认特性:析构行为(反构造功能)、sysfs行为(sysfs的操作表)以及别的一些默认属性。

kset:kset集合可以将kobject归入,kset集合由struct kset结构体表示。提供如下两个功能:

  1. 嵌入的kobject作为kobject组的积累;
  2. kset将相关的kobject集合在一起

kobject、ktype和kset的相互关系如图所示:
【读书笔记】Linux内核设计与实现--设备与模块_第1张图片

3.5 管理和操作kobject

通常多数时候,驱动程序并不必直接处理kobject,因为kobject是被潜入到一些特殊类型结构体中,而且会由相关的设备驱动程序在“幕后”管理。

Q:如何管理和操作kobject?
A:
声明和初始化kobject,通过函数kobject_init进行初始化,定义在文件中:

void kobject_init(struct kobject *obj, struct kobj_type *ktype);

kobj:需要初始化的kobject对象,再调用初始化函数前,该对象必须清空,使用memset()可以完成清空工作;

ps:大多数清空下,通常使用 kobject_create()完成上述多步操作(初始化,清空等)。

3.6 引用计数–kobject的主要功能之一

引用计数系统被初始化后,kobject的引用计数设置为1。
只要引用计数不为零,那么该对象就会继续保留在内存中。
任何包含对象引用的代码首先要增加该对象的引用计数,当代码结束后则减少它的引用计数。
增加引用计数称为获得(getting)对象的引用,减少引用计数称为释放(putting)对象的引用。
当引用计数跌到零时,对象便可以被撤销,同时相关内存也都被释放。

Q:如何操作引用计数?
A:
递增引用计数:

struct kobject * kobject_get(struct kobject *kobj);

递减引用计数:

void kobject_put(struct kobject *kobj);	/* 如果对于的kobject的引用计数减少到零,则于该kobject管理的ktype中的析构函数将被调用 */

kobject的引用计数是通过kref结构体实现的,该结构体定义在文件中:

struct kref {
	atomic_t refcount;		/* 存放引用计数的原子变量 */
};

Q:kref结构体里就一个唯一的成员变量,为啥采用结构体?
A:因为在使用kref前,必须先对kref进行类型检测,检测的函数为kref_init(struct kref *kref);

kref的相关操作函数如下所示,声明在

函数 描述
void kref_get(struct kref *kref) 获得对kref的引用
int kref_put(struct kref *kref, void(*release)(struct kref *kref)) 减少对kref的引用,每次调用引用计数减少1,如果减少至零,则要调用作为参数提供的release()函数

4.sysfs–处于内存中的虚拟文件系统

sysfs文件系统是一个处于内存中的虚拟文件系统,提供了kobject对象层次结构的视图。
sysfs代替了先前处于/proc下的设备相关文件,并且为系统对象提供了一个很有效的视图。
sysfs的诀窍是把kobject对象与目录项(directory entries)紧密联系起来,通过kobject对象中的dentry字段实现的。

sysfs文件系统挂载在sys目录下,sysfs的根目录包含了至少十个目录:block、bus、class、dev、devices、firmware、fs、kernel、module和power。

block目录下的每个子目录都对应着系统中的一个已注册的块设备;
bus目录提供了一个系统的总线视图;
class目录包含了以高层功能逻辑组织起来的系统设备视图;
dev目录是已注册设备节点的视图;
devices目录是系统中设备拓扑结构视图,直接映射出内核中设备结构体的组织层次;
firmware目录包含了一些如ACPI、EDD、EFI等低层子系统的特殊树;
fs目录是已注册文件系统的视图;
kernel目录包含内核配置项和状态信息;
module目录则包含系统已加载模块的信息;
power目录包含系统范围的电源管理数据;

ps:上述目录中最重要的是devices,该目录将设备模型导出到用户空间。目录结构就是系统中实际的设备拓扑。其他目录中的很多数据将devices目录下的数据加以转换加工而得。

4.1 sysfs中添加和删除kobject–kobject_add()/kobject_del()

在初始化kobject之后是不能自动将其导出到sysfs中,想要把kobject导入sysfs,需要用到kobject_add()

int kobject_add(struct kobject *obj, struct kobject *parent, const char *fmt, ...);

kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。
如果kobject的父指针被设置,那么在sysfs中kobject将被映射为其父目录下的子目录;
如果parent没有设置,那么kobject将被映射为kset->kobj中的子目录;
如果给定的kobject中的parent或kset字段都没有被设置,那么就认为kobject没有父对象,所以就会被映射成sysfs下的根级目录。

kobject的目录名字是由fmt指定的,它也接收printf()样式的格式化字符串。

ps:辅助函数kobject_create_and_add()把kobject_create()和kobject_add()所做的工作放在一个函数中:

struct kobject *kobject_create_and_add(const char *name,struct kobject *parent);
/*kobject_create_and_add函数接受直接的指针name作为kobject所对应的目录名称,而kobject_add函数使用printf()风格的格式化字符串*/

Q:如何从sysfs中删除一个kobject对应文件目录?
A:使用函数kobject_del()

void kobject_del(struct kobject *kobj);

4.2 向sysfs中添加文件

kobject被映射成文件目录所有的对象层次结构都映射成sys下的目录结构

ps:
sysfs仅仅是一个树,没有提供实际数据的文件。

Q:文件从何而来?
A:
1.默认属性:
默认的文件集合是通过kobject和kset中的ktype字段提供的
kobj_type字段含有一个字段–default_attrs,它是一个attribute结构体数组,这些属性负责将内核数据映射成sysfs中的文件

attribute结构体定义在文件中:

struct attribute {
	const char		*name;					/* 属性名称,出现在sysfs的文件名 */
	umode_t			mode;					/* 权限:表示了sysfs中该文件的权限,如S_IRUGO等 */
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	bool			ignore_lockdep:1;
	struct lock_class_key	*key;
	struct lock_class_key	skey;
#endif
};

ps:
sysfs中的所有文件和目录的uid与gid标志均为零。

kobj_type字段中的sysfs_ops字段描述了如何使用default_attrs字段列出的默认属性,sysfs_ops字段指向了一个定义域文件的同名结构体:

struct sysfs_ops {
	ssize_t	(*show)(struct kobject *, struct attribute *,char *);	/* 在读sysfs文件时该方法被调用 */
	ssize_t	(*store)(struct kobject *,struct attribute *,const char *, size_t);	/* 在写sysfs文件时该方法被调用 */
	const void *(*namespace)(struct kobject *, const struct attribute *);
};

2.创建新属性
通常由kobject相关ktype所提供的默认属性是充足的,但是有时候在一些特别清空下会碰到特殊的kobject实例需要有自己的属性,sysfs_create_file()接口满足了该需求。

int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);	/* 成功返回零,失败返回错误码 */

attr:attr参数指向相应的attribute结构体;
kobj:kobj参数指定了属性所在的kobject对象。

Q:如何在sysfs中创建一个符号连接?
A:int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);,该函数创建的符号连接名由name指定,连接则由kobj对应的目录映射到target指定的目录。如果成功函数返回零,如果失败返回负的错误码。

3.删除新属性

void sysfs_remove_file(struct kobject *kobj, const struct atrtribute *attr);	/* 删除给定的kobj对应目录中的属性 */
void sysfs_remove_link(struct kobject *kobj, char *name);	/* 删除kobj对应目录中的名为name的符号连接 */

4.sysfs约定
sysfs文件系统代替了以往需要由ioctl()(作用于设备节点)和procfs文件系统完成的功能。
这种代替避免了在调用ioctl()时使用类型不正确的参数和弄乱/proc目录结构。
因此为了保持sysfs干净和直观,需遵循以下约定:

  1. sysfs属性应保证每个文件只导出一个值,该值应该是文本形式而且映射为简单C类型;
  2. sysfs中要以一个清晰的层次组织数据;
  3. 记住sysfs提供内核到用户空间的服务,有些用户空间的ABI(应用程序二进制接口)的作用。

4.3 内核事件层–建立在kobject基础上的内核到用户的消息通知系统

内核事件层把事件模拟为信号–从明确的kobject对象发出,所以每个事件源都是要给sysfs路径。
每个事件都被赋予一个动词或者字符串表示信号。
每个事件都有一个可选的负载(payload),内核事件层使用sysfs属性代表负载。

从内部实现上来讲,内核事件层由内核空间传递到用户空间需要经过netlink。
netlink是一个用于传送网络信息的多点传送套接字。使用方法是用户空间实现一个系统后台服务用于监听套接字,处理任何读到的信息,并将事件传送到系统栈里。

Q:如何在内核代码中向用户空间发送信号?
A:

int kobject_uevent(struct kobject *kobj, enum kobject_action action);

kobj:参数kobj指定发送该信号的kobject对象,实际的内核事件将包含该kobject映射到sysfs的路径;
action:参数action指定了描述该信号的“动作”或者“动词”。实际的内核事件将包含一个映射成枚举类型kobject_action的字符串,而且也消除了打字错误或其他错误。该枚举变量定义于文件中,其形式为kOBJ_foo。

/*
 * The actions here must match the index to the string array
 * in lib/kobject_uevent.c
 *
 * Do not add new actions here without checking with the driver-core
 * maintainers. Action strings are not meant to express subsystem
 * or device specific properties. In most cases you want to send a
 * kobject_uevent_env(kobj, KOBJ_CHANGE, env) with additional event
 * specific variables added to the event environment.
 */
enum kobject_action {
	KOBJ_ADD,
	KOBJ_REMOVE,
	KOBJ_CHANGE,
	KOBJ_MOVE,
	KOBJ_ONLINE,
	KOBJ_OFFLINE,
	KOBJ_MAX
};

你可能感兴趣的:(Linux内核设计与实现)