阅读:1722回复:4
Linux2.6使用点滴
深入分析Linux内核链表
链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。 通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图: 1. 单链表 图1 单链表 单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。 2. 双链表 图2 双链表 通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。 3. 循环链表 循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。 在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。 二、 Linux 2.6内核链表数据结构的实现 尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和HASH链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下 rcu和 hlist。 链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件): struct list_head {struct list_head *next, *prev;}; list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。 和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。 在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例): struct list_node {struct list_node *next;ElemTypedata;}; 因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的C++程序员应该知道,标准模板库中的<list>采用的是C++ Template,利用模板抽象出和数据项类型无关的链表操作接口。 在Linux 内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述 Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(struct list_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在 [net/core/netfilter.c]中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。 图3 nf_sockopts链表示意图 三、 链表操作接口 1. 声明和初始化 实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏: #define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) 当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空: static inline int list_empty(const struct list_head *head){return head->next == head;} 除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表: #define INIT_LIST_HEAD(ptr) do { \(ptr)->next = (ptr); (ptr)->prev = (ptr); \} while (0) 我们用INIT_LIST_HEAD(&nf_sockopts)来使用它。 2. 插入/删除/合并 a) 插入 对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口: static inline void list_add(struct list_head *new, struct list_head *head);static inline void list_add_tail(struct list_head *new, struct list_head *head); 因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用 __list_add(new, head, head->next); 和__list_add(new, head->prev, head); 来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。 假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作: list_add(&new_sockopt.list, &nf_sockopts); 从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。 b) 删除 static inline void list_del(struct list_head *entry); 当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作: list_del(&new_sockopt.list); 被剔除下来的new_sockopt.list, prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。 c) 搬移 Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类: static inline void list_move(struct list_head *list, struct list_head *head);static inline void list_move_tail(struct list_head *list, struct list_head *head); 例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。 d) 合并 除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能: static inline void list_splice(struct list_head *list, struct list_head *head); 假设当前有两个链表,表头分别是list1和 list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针): 图4 链表合并list_splice(&list1,&list2) 当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数: static inline void list_splice_init(struct list_head *list, struct list_head *head); 该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。 3. 遍历 遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。 a) 由链表节点到数据项变量 我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问 nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用: list_entry(nf_sockopts->next, struct nf_sockopt_ops, list); 这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。 list_entry的使用相当简单,相比之下,它的实现则有一些难懂: #define list_entry(ptr, type, member) container_of(ptr, type, member)container_of宏定义在[include/linux/kernel.h]中:#define container_of(ptr, type, member) ({\const typeof( ((type *)0)->member ) *__mptr = (ptr);\(type *)( (char *)__mptr - offsetof(type,member) );})offsetof宏定义在[include/linux/stddef.h]中:#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) size_t最终定义为unsigned int(i386)。 这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。 container_of ()和offsetof()并不仅用于链表操作,这里最有趣的地方是((type *)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。在container_of 宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在 offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。 如果这么说还不好理解的话,不妨看看下面这张图: 图5 offsetof()宏的原理 对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。 b) 遍历宏 在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话: ……struct list_head *i;……list_for_each(i, &nf_sockopts) {struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;……}…… 函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在[include/linux/list.h]中,list_for_each()宏是这么定义的: #define list_for_each(pos, head) \for (pos = (head)->next, prefetch(pos->next); pos != (head); \pos = pos->next, prefetch(pos->next)) 它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。 那么在nf_register_sockopt()中实际上就是遍历nf_sockopts链表。为什么能直接将获得的list_head成员变量地址当成 struct nf_sockopt_ops数据项变量的地址呢?我们注意到在struct nf_sockopt_ops结构中,list是其中的第一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是: struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list); 大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏: #define list_for_each_entry(pos, head, member)…… 与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。nf_register_sockopt()函数可以利用这个宏而设计得更简单: ……struct nf_sockopt_ops *ops;list_for_each_entry(ops,&nf_sockopts,list){……}…… 某些应用需要反向遍历链表,Linux提供了 list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的 list_for_each()、list_for_each_entry()完全相同。 如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos, head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此, Linux专门提供了一个 list_prepare_entry(pos,head,member)宏,将它的返回值作为 list_for_each_entry_continue()的pos参数,就可以满足这一要求。 4. 安全性考虑 在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面: a) list_empty()判断 基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful ()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、 prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。 b) 遍历时节点删除 前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。 当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe" 接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。 四、 扩展 1. hlist 图6 list和hlist 精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构 --单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。 因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的 next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的"*(node->pprev)"访问和修改前驱节点的next(或first)指针。 2. read-copy update 在Linux链表功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。 我们知道,系统中数据读取操作远多于写操作,而rwlock机制在smp环境下随着处理机增多性能会迅速下降。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,;而对RCU链表的使用和基本链表的使用方法基本相同。 五、 示例 附件中的程序除了能正向、反向输出文件以外,并无实际作用,仅用于演示Linux链表的使用。 为了简便,例子采用的是用户态程序模板,如果需要运行,可采用如下命令编译: gcc -D__KERNEL__ -I/usr/src/linux-2.6.7/include pfile.c -o pfile 因为内核链表限制在内核态使用,但实际上对于数据结构本身而言并非只能在核态运行,因此,在笔者的编译中使用"-D__KERNEL__"开关"欺骗"编译器。 [不会插入图片,抱歉] |
|
|
沙发#
发布于:2007-04-13 13:53
Linux系统单一内核模块编译过程解析
单一模块编译,想象两个情况: 如果我的预设核心忘记加入某个功能,而且该功能可以编译成为模块,不过, 预设核心却也没有将该项功能编译成为模块,害我不能使用时,该如何是好? 如果Linux 核心原始码并没有某个硬件的驱动程序 (module) ,但是开发该硬件的厂商有提供给 Linux 使用的驱动程序原始码,那么我又该如何将该项功能编进核心模块呢? 很有趣对吧!不过,在这样的情况下其实没有什么好说的,反正就是去取得原始码后,重新编译成为系统可以加载的模块啊!很简单,对吧! 但是,上面那两种情况的模块编译行为是不太一样的,不过,都是需要 make, gcc 以及核心所提供的 include 标头档与函式库等等。 硬件开发商提供的额外模块: 很多时候,可能由于核心预设的核心驱动模块所提供的功能您不满意, 或者是硬件开发商所提供的核心模块具有更强大的功能, 又或者该硬件是新的,所以预设的核心并没有该硬件的驱动模块时,那您只好自行由硬件开发商处取得驱动模块, 然后自行编译啰! 如果您的硬件开发商有提供驱动程序的话,那么真的很好解决,直接下载该原始码,重新编译, 将他放置到核心模块该放置的地方后,呵呵!就能够使用了!举例来说,如果您不想使用核心原本提供的 Intel 网络卡模块,而想使用 Intel 官方释出的最新模块,例如下面这个例子: 模块说明与下载:http: //downloadfinder.intel.com/ ... l/Detail_Desc.aspx? agr=Y&Inst=Yes&ProductID=993&DwnldID=2896&strOSs=39&OSFullName=Linux*〈=eng 您可以利用各种方法将他下载后,假设这个档案放置到 /root ,那么直接将他解压缩吧! 之后就可以读一读 INSTALL/README ,然后找一下 Makefile ,就能够编译了。整体流程有点像这样: 1. 将档案解压缩: [root@linux ~]# cd /usr/local/src[root@linux src]# tar -zxvf /root/e100-3.4.14.tar.gz[root@linux src]# cd e100-3.4.14 2. 开始进行编译与安装: [root@linux e100-3.4.14]# vi README <==注意查一下该档案内容[root@linux e100-3.4.14]# cd src[root@linux src]# make # 此时您会看到出现如下这一行: # make[1]: Entering directory `/usr/src/kernels/2.6.13-1.1532_FC4-i686' # 这代表这个驱动程序在编译时,会去读取的核心原始码 include file # 的目录所在!有兴趣的朋友,务必查阅一下 Makefile 啦! [root@linux src]# ls -l-rw-r--r-- 1 root root 77908 Jul 2 08:24 e100.c-rw-r--r-- 1 root root 351351 Dec 5 00:48 e100.ko-rw-r--r-- 1 root root 4775 Dec 5 00:48 e100.mod.c-rw-r--r-- 1 root root 39684 Dec 5 00:48 e100.mod.o-rw-r--r-- 1 root root 312564 Dec 5 00:48 e100.o-rw-r--r-- 1 root root 21092 Jul 2 08:24 ethtool.c-rw-r--r-- 1 root root 43258 Jul 2 08:24 kcompat.h-rw-r--r-- 1 root root 9610 Jul 2 08:24 Makefile 3. 开始将该模块移动到核心目录,并且更新模块相依属性! [root@linux src]# cp e100.ko \> /lib/modules/`uname -r`/kernel/drivers/net[root@linux src]# cd /lib/modules/`uname -r`[root@linux 2.6.13-1.1532_FC4]# depmod -a 有趣吧!透过这样的动作,我们就可以轻易的将模块编译起来,并且还可以将他直接放置到核心模块目录中, 同时以depmod 将模块建立相关性,未来就能够利用modprobe 来直接取用啦!^_^ 但是需要提醒您的是,当自行编译模块时, 若您的核心有更新 (例如利用自动更新机制进行线上更新) 时,则您必须要重新编译该模块一次,重复上面的步骤!才行!因为这个模块仅针对目前的核心来编译的啊!对吧! 利用旧有的核心原始码进行编译: 举个例子来说,目前FC4 的核心就是 2.6 版,而且也有 NTFS 的原始码,只不过, FC4 就是没有将这个模块给他编译起来!那我能否使用目前的核心原始码进行NTFS 档案系统的模块编译呢?当然可以啊!不过,我是否需要整个核心编译的过程从头来一次呢? 我们首先到目前的核心原始码所在目录下达 make menuconfig , 然后将 NTFS 的选项设定成为模块,之后直接下达: make fs/ntfs/ 那么ntfs 的模块就会自动的被编译出来了!可惜的是,预设的 FC4 核心原始码并没有附上所有的程序代码, 仅有提供相关的 Makefile 档案而已,伤脑筋~ 因此,您仅能以我们刚刚才建立的 /usr/src/linux-2.6.14.2 这个目录, 直接下达 make fs/ntfs 来建立起 ntfs.ko 这个模块~然后将该模块复制到 /lib/modules/2.6.14.2/kernel/fs/ntsf/ 目录下, 再去到 /lib/modules/2.6.14.2 底下执行 depmod -a ,呵呵~ 就可以在原来的核心底下新增某个想要加入的模块功能啰。 核心模块管理: lsmod, modinfo, modprobe, insmod, rmmod... 核心与核心模块是分不开的,至于驱动程序模块在编译的时候,更与核心的原始码功能分不开~ 因此,您必须要先了解到:核心、核心模块、驱动程序模块、核心原始码与标头档案的相关性,然后才有办法了解到为何编译驱动程序的时候老是需要找到核心的原始码才能够顺利编译!然后也才会知道,为何当核心更新之后,自己之前所编译的核心模块会失效~ 此外,与核心模块有相关的,还有那个很常被使用的 modprobe 指令,以及开机的时候会读取到的模块定义数据文件 /etc/modprobe.conf ,这些资料您也必须要了解才行~相关的指令说明我们已经在开机流程与 loader 文章内谈过了, 您应该要自行前往了解。 |
|
|
板凳#
发布于:2007-04-13 13:55
关于Linux操作系统的NTFS和内核分析
传统编译内核模块的方法繁琐而费时,本文将告诉我们一种快速编译所需要内核模块的新方法。当你安装完Linux系统,并且已经启动,恭喜你!如果你的硬盘上还安装了WinNT/2000系统,你试图去访问另一个NTFS分区时却遇到了麻烦。因为你所用的Linux系统没有已编译的支持NTFS文件系统的模块。怎么办?也许你会运行make menuconfig,重新定制你需要的所有模块,接着运行make modeules;make modeules_install来安装。这样不仅繁琐、费时,还可能会出现问题。或者因为编译内核对你有些棘手,太多的选择让你手足无措,你根本没有太好的方法。本文给你提供一个简单的方法,你可以轻松地去编译你所需要的支持NTFS系统的模块(ntfs.o)。以此为例,但愿对你编译其他模块有所帮助。 写此文时我用的系统是Red Hat Linux release 7.0 (Guinness) Kernel 2.2.16-22 on an i686。从一个新安装的系统开始,我们一起去编译一个自己想要的支持NTFS文件系统模块。 一、找到编译内核所需要的.config文件 在/usr/src/linux/configs目录下有若干编译内核所用的配置。选择我们想要的配置,将它复制到/usr/src/linux目录下,改名为.config。 cp /usr/src/linux/configs/kernel-2.2.16-i686.config /usr/src/linux/.config 二、修改.config文件,去掉不用的模块,加上自己想要的模块 打开.config,有许多XXXX=m的项,这些都是要被编译为模块的项,因为我们不希望编译这些模块,所以要把XXXX=m的项统统去掉。然后再加上我们想要的模块,将# CONFIG_NTFS_FS is not set 改为CONFIG_NTFS_FS=m 当然,可以用你熟悉各种工具来做这件事。 三、编译NTFS模块 在/usr/src/linux目录下运行命令make modules来编译我们想要的NTFS模块。 四、安装NTFS模块 编译后得到的ntfs.o在/usr/src/linux/fs/ntfs目录下,手动将它复制到正确的目录下。 cp /usr/src/linux/fs/ntfs/ntfs.o /lib/modules/2.2.16-22/fs/ 注意:千万不能运行命令make modules_install,否则将带来严重的后果,它会删除你系统中的所有模块,只安装刚刚编译的模块(ntfs.o)。 五、载入NTFS模块 运行命令depmod;modprobe ntfs 试着访问你的NTFS文件系统吧,祝你成功! 有些模块依赖于你的系统内核,所以不适用本文所提供的方法。还有些模块和其他模块有依赖关系。如果你不熟悉这些依赖关系的话,建议你在第二步去掉不用的模块选项后,通过make menuconfig来加上自己想要的模块。 我用此方法用了三分钟编译了支持NTFS文件系统的模块,你呢? |
|
|
地板#
发布于:2007-04-13 13:59
Linux操作系统内核抢占补丁的基本原理
【导读】 CPU在内核中运行时并不是处处不可抢占的,内核中存在一些空隙,在这时进行抢占是安全的,内核抢占补丁的基本原理就是将SMP可并行的代码段看成是可以进行内核抢占的区域。 [被屏蔽广告]CPU在内核中运行时并不是处处不可抢占的,内核中存在一些空隙,在这时进行抢占是安全的,内核抢占补丁的基本原理就是将SMP可并行的代码段看成是可以进行内核抢占的区域。 2.4内核正好细化了多CPU下的内核线程同步机构,对不可并行的指令块用spinlock和rwlock作了细致的表示,该补丁的实现可谓水到渠成。具体的方法就是在进程的任务结构上增加一个preempt_count变量作为内核抢占锁,它随着spinlock和rwlock一起加锁和解锁。当preempt_count为0时表示可以进行内核调度。内核调度器的入口为preempt_schedule(),它将当前进程标记为TASK_PREEMPTED状态再调用schedule(),在TASK_PREEMPTED状态,schedule()不会将进程从运行队列中删除。 下面是内核抢占补丁的主要代码示意: arch/i386/kernel/entry.S: preempt_count = 4 # 将task_struct中的flags用作preempt_count,flags被移到了别 的位置 ret_from_exception: # 从异常返回 #ifdef CONFIG_SMP GET_CURRENT(%ebx) movl processor(%ebx),%eax shll $CONFIG_X86_L1_CACHE_SHIFT,%eax movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask #else movl SYMBOL_NAME(irq_stat),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask #endif jne handle_softirq #ifdef CONFIG_PREEMPT cli incl preempt_count(%ebx) # 异常的入口没有禁止内核调度的指令,与ret_from_intr 匹配一下 #endif ENTRY(ret_from_intr) # 硬件中断的返回 GET_CURRENT(%ebx) #ifdef CONFIG_PREEMPT cli decl preempt_count(%ebx) # 恢复内核抢占标志 #endif movl EFLAGS(%esp),%eax # mix EFLAGS and CS movb CS(%esp),%al testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? jne ret_with_reschedule #ifdef CONFIG_PREEMPT cmpl $0,preempt_count(%ebx) jnz restore_all # 如果preempt_count非零则表示禁止内核抢占 cmpl $0,need_resched(%ebx) jz restore_all # movl SYMBOL_NAME(irq_stat)+irq_stat_local_bh_count CPU_INDX,%ecx addl SYMBOL_NAME(irq_stat)+irq_stat_local_irq_count CPU_INDX,%ecx jnz restore_all incl preempt_count(%ebx) sti call SYMBOL_NAME(preempt_schedule) jmp ret_from_intr # 新进程返回,返回ret_from_intr恢复抢占标志后再返回 #else jmp restore_all #endif ALIGN handle_softirq: #ifdef CONFIG_PREEMPT cli GET_CURRENT(%ebx) incl preempt_count(%ebx) sti #endif call SYMBOL_NAME(do_softirq) jmp ret_from_intr ALIGN reschedule: call SYMBOL_NAME(schedule) # test jmp ret_from_sys_call include/asm/hw_irq.h: ... #ifdef CONFIG_PREEMPT #define BUMP_CONTEX_SWITCH_LOCK \ GET_CURRENT \ "incl 4(%ebx)\n\t" #else #define BUMP_CONTEX_SWITCH_LOCK #endif #define SAVE_ALL \ 硬件中断保护入口现场 "cld\n\t" \ "pushl %es\n\t" \ "pushl %ds\n\t" \ "pushl %eax\n\t" \ "pushl %ebp\n\t" \ "pushl %edi\n\t" \ "pushl %esi\n\t" \ "pushl %edx\n\t" \ "pushl %ecx\n\t" \ "pushl %ebx\n\t" \ "movl $" STR(__KERNEL_DS) ",%edx\n\t" \ "movl %edx,%ds\n\t" \ "movl %edx,%es\n\t" \ BUMP_CONTEX_SWITCH_LOCK # 硬件中断的入口禁止内核抢占 include/linux/spinlock.h: #ifdef CONFIG_PREEMPT #define switch_lock_count() current->preempt_count #define in_ctx_sw_off() (switch_lock_count().counter) 判断当前进程的抢占计数 是否非零 #define atomic_ptr_in_ctx_sw_off() (&switch_lock_count()) #define ctx_sw_off() \ 禁止内核抢占 do { \ atomic_inc(atomic_ptr_in_ctx_sw_off()); \ 当前进程的内核抢占计数增1 } while (0) #define ctx_sw_on_no_preempt() \ 允许内核抢占 do { \ atomic_dec(atomic_ptr_in_ctx_sw_off()); \ 当前进程的内核抢占计数减1 } while (0) #define ctx_sw_on() \ 允许并完成内核抢占 do { \ if (atomic_dec_and_test(atomic_ptr_in_ctx_sw_off()) && \ current->need_resched) \ preempt_schedule(); \ } while (0) #define spin_lock(lock) \ do { \ ctx_sw_off(); \ 进入自旋锁时禁止抢占 _raw_spin_lock(lock); \ } while(0) #define spin_trylock(lock) ({ctx_sw_off(); _raw_spin_trylock(lock) ? \锁定并 测试原来是否上锁 1 : ({ctx_sw_on(); 0;});}) #define spin_unlock(lock) \ do { \ _raw_spin_unlock(lock); \ ctx_sw_on(); \ 离开自旋锁时允许并完成内核抢占 } while (0) #define read_lock(lock) ({ctx_sw_off(); _raw_read_lock(lock);}) #define read_unlock(lock) ({_raw_read_unlock(lock); ctx_sw_on();}) #define write_lock(lock) ({ctx_sw_off(); _raw_write_lock(lock);}) #define write_unlock(lock) ({_raw_write_unlock(lock); ctx_sw_on();}) #define write_trylock(lock) ({ctx_sw_off(); _raw_write_trylock(lock) ? \ 1 : ({ctx_sw_on(); 0;});}) ... include/asm/softirq.h: #define cpu_bh_disable(cpu) do { ctx_sw_off(); local_bh_count(cpu)++; barrie r(); } while (0) #define cpu_bh_enable(cpu) do { barrier(); local_bh_count(cpu)--;ctx_sw_on() ; } while (0) kernel/schedule.c: #ifdef CONFIG_PREEMPT asmlinkage void preempt_schedule(void) { while (current->need_resched) { ctx_sw_off(); current->state |= TASK_PREEMPTED; schedule(); current->state &= ~TASK_PREEMPTED; ctx_sw_on_no_preempt(); } } #endif asmlinkage void schedule(void) { struct schedule_data * sched_data; struct task_struct *prev, *next, *p; struct list_head *tmp; int this_cpu, c; #ifdef CONFIG_PREEMPT ctx_sw_off(); #endif if (!current->active_mm) BUG(); need_resched_back: prev = current; this_cpu = prev->processor; if (in_interrupt()) goto scheduling_in_interrupt; release_kernel_lock(prev, this_cpu); /* Do "administrative" work here while we don't hold any locks */ if (softirq_active(this_cpu) & softirq_mask(this_cpu)) goto handle_softirq; handle_softirq_back: /* * 'sched_data' is protected by the fact that we can run * only one process per CPU. */ sched_data = & aligned_data[this_cpu].schedule_data; spin_lock_irq(&runqueue_lock); /* move an exhausted RR process to be last.. */ if (prev->policy == SCHED_RR) goto move_rr_last; move_rr_back: switch (prev->state) { case TASK_INTERRUPTIBLE: if (signal_pending(prev)) { prev->state = TASK_RUNNING; break; } default: #ifdef CONFIG_PREEMPT if (prev->state & TASK_PREEMPTED) break; 如果是内核抢占调度,则保留运行队列 #endif del_from_runqueue(prev); #ifdef CONFIG_PREEMPT case TASK_PREEMPTED: #endif case TASK_RUNNING: } prev->need_resched = 0; /* * this is the scheduler proper: */ repeat_schedule: /* * Default process to select.. */ next = idle_task(this_cpu); c = -1000; if (task_on_runqueue(prev)) goto still_running; still_running_back: list_for_each(tmp, &runqueue_head) { p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p, this_cpu)) { int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c) c = weight, next = p; } } /* Do we need to re-calculate counters? */ if (!c) goto recalculate; /* * from this point on nothing can prevent us from * switching to the next task, save this fact in * sched_data. */ sched_data->curr = next; #ifdef CONFIG_SMP next->has_cpu = 1; next->processor = this_cpu; #endif spin_unlock_irq(&runqueue_lock); if (prev == next) goto same_process; #ifdef CONFIG_SMP /* * maintain the per-process 'last schedule' value. * (this has to be recalculated even if we reschedule to * the same process) Currently this is only used on SMP, * and it's approximate, so we do not have to maintain * it while holding the runqueue spinlock. */ sched_data->last_schedule = get_cycles(); /* * We drop the scheduler lock early (it's a global spinlock), * thus we have to lock the previous process from getting * rescheduled during switch_to(). */ #endif /* CONFIG_SMP */ kstat.context_swtch++; /* * there are 3 processes which are affected by a context switch: * * prev == .... ==> (last => next) * * It's the 'much more previous' 'prev' that is on next's stack, * but prev is set to (the just run) 'last' process by switch_to(). * This might sound slightly confusing but makes tons of sense. */ prepare_to_switch(); { struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (!mm) { if (next->active_mm) BUG(); next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next, this_cpu); } else { if (next->active_mm != mm) BUG(); switch_mm(oldmm, mm, next, this_cpu); } if (!prev->mm) { prev->active_mm = NULL; mmdrop(oldmm); } } /* * This just switches the register state and the * stack. */ switch_to(prev, next, prev); __schedule_tail(prev); same_process: reacquire_kernel_lock(current); if (current->need_resched) goto need_resched_back; #ifdef CONFIG_PREEMPT ctx_sw_on_no_preempt(); #endif return; recalculate: { struct task_struct *p; spin_unlock_irq(&runqueue_lock); read_lock(&tasklist_lock); for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice); read_unlock(&tasklist_lock); spin_lock_irq(&runqueue_lock); } goto repeat_schedule; still_running: c = goodness(prev, this_cpu, prev->active_mm); next = prev; goto still_running_back; handle_softirq: do_softirq(); goto handle_softirq_back; move_rr_last: if (!prev->counter) { prev->counter = NICE_TO_TICKS(prev->nice); move_last_runqueue(prev); } goto move_rr_back; scheduling_in_interrupt: printk("Scheduling in interrupt\n"); BUG(); return; } void schedule_tail(struct task_struct *prev) { __schedule_tail(prev); #ifdef CONFIG_PREEMPT ctx_sw_on(); #endif } |
|
|
地下室#
发布于:2007-04-24 09:42
还没入门,看不懂,顶一下
|
|