内核的基础层和应用层

◆ 第 1 章 内核的基础层和应用层
1.1.1 内核中使用内存
简单说,内核提供了两个层次的内存分配接口。一个是从伙伴系统分配,另一个是从
slab 系统分配。伙伴系统是最底层的内存管理机制,提供页式的内存管理,而 slab 是伙伴系
统之上的内存管理,提供基于对象的内存管理。
从伙伴系统分配内存的调用是 alloc_pages,注意此时得到的是页面地址,如果要获得能
使用的内存地址,还需要用 page_address 调用来获得内存地址。
如果要直接获得内存地址,需要使用 __get_free_pages。__get_free_pages 其实封装了
alloc_pages 和 page_address 两个函数。
alloc_pages 申请的内存是以页为单元的,最少要一个页。如果只是申请一小块内存,一
个页就浪费了,而且内核中很多应用也希望一种对象化的内存管理,希望内存管理能自动地
构造和析构对象,这都很接近面向对象的思路了,这就是 slab 内存管理。
要从 slab 申请内存,需要创建一个 slab 对象,使用 kmem_cache_create 创建 slab 对象。
kmem_cache_create 可以提供对象的名字和大小、构造函数和析构函数等,然后通过 kmem_
cache_alloc 和 kmem_cache_free 来申请和释放内存。
内核中常用的 kmalloc 其实也是 slab 提供的对象管理,只不过内核已经构建了一些固定
大小的对象,用户通过 kmalloc 申请的时候,就使用了这些对象。
一个内核中创建 slab 对象的例子如代码清单 1-1 所示。
代码清单 1-1 创建 slab 对象
bh_cachep = kmem_cache_create("buffer_head",
sizeof(struct buffer_head), 0,
(SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD),
init_buffer_head,
NULL);
创建一个 slab 对象时指定了 slab 对象的大小,用以下代码申请一个 slab 对象:
struct buffer_head *ret = kmem_cache_alloc(bh_cachep, gfp_?ags);
内核中还有一个内存分配调用:vmalloc。vmalloc 的作用是把物理地址不连续的内存页
面拼凑为逻辑地址连续的内存区间。
理解了以上几个函数调用之后,阅读内核代码的时候就可以清晰内核中对内存的使用
方式。
1.1.2 内核中的任务调度
内核中经常需要进行进程的调度。首先看一个例子,如代码清单 1-2 所示。
代码清单 1-2 使用 wait 的任务调度
#de?ne wait_event(wq, condition)
do {
if (condition)1.1 内核基础层提供的服务 ◆ 3
break;
__wait_event(wq, condition);
} while (0)
#de?ne __wait_event(wq, condition)
do {
DEFINE_WAIT(__wait);
for (;;) {
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);
if (condition)
break;
schedule();
}
?nish_wait(&wq, &__wait);
} while (0)
上文定义了一个 wait 结构,然后设置进程睡眠。如果有其他进程唤醒这个进程后,判断
条件是否满足,如果满足,删除 wait 对象,否则进程继续睡眠。
这是一个很常见的例子,使用 wait_event 实现进程调度的实例在内核中很多,而且内核
中还实现了一系列函数,简单介绍如下。
?wait_event_timeout :和 wait_event 的区别是有时间限制,如果条件满足,进程恢复运
行,或者时间到达,进程同样恢复运行。
?wait_event_interruptible :和 wait_event 类似,不同之处是进程处于可中断的睡眠。而
wait_event 设置进程处于不可中断的睡眠。两者区别何在?可中断的睡眠进程可以接
收到信号,而不可中断的睡眠进程不能接收信号。
?wait_event_interruptible_timeout :和 wait_event_interruptible 相比,多个时间限制。在
规定的时间到达后,进程恢复运行。
?wait_event_interruptible_exclusive:和 wait_event_interruptible 区别是排他性的等待。
注意
何谓排他性的等待?有一些进程都在等待队列中,当唤醒的时候,内核是唤醒所有的进
程。如果进程设置了排他性等待的标志,唤醒所有非排他性的进程和一个排他性进程。
1.1.3 软中断和 tasklet
Linux 内核把对应中断的软件执行代码分拆成两部分。一部分代码和硬件关系紧密,这
部分代码必须关闭中断来执行,以免被后面触发的中断打断,影响代码的正确执行,这部分
代码放在中断上下文中执行。另一部分代码和硬件关系不紧密,可以打开中断执行,这部分
代码放在软中断上下文执行。
需要指出的是,这种划分是一种粗略、大概的划分。中断是计算机系统的宝贵资源,关
闭中断意味着系统不响应中断,这常常是代价高昂的。所以为了避免关闭中断的不利影响,4
◆ 第 1 章 内核的基础层和应用层
即使在中断上下文中,也有很多代码的执行是打开中断的。而在软中断上下文,甚至进程上
下文的内核代码中,有的时候也是需要关闭中断的。哪些地方需要关闭中断,而哪些地方又
可以打开中断,需要仔细地考虑,既要尽可能打开中断以防止关闭中断的不利影响,又要在
需要的时候关闭中断以避免出现错误。
Linux内核定义了几个默认的软中断,网络设备有自己的发送和接收软中断,块设备也有
自己的软中断。为了方便使用,内核还定义了一个tasklet软中断。tasklet是一种特殊的软中断,
同一时刻一个tasklet只能有一个CPU 执行,不同的tasklet可以在不同的CPU上执行。这和软
中断不同,软中断同一时刻可以在不同的CPU并行执行,因此软中断必须考虑重入的问题。
内核中很多地方使用了 tasklet。分析一个例子,代码如下所示:
DECLARE_TASKLET_DISABLED(hil_mlcs_tasklet, hil_mlcs_process, 0);
tasklet_schedule(&hil_mlcs_tasklet);
上面的例子首先定义了一个 tasklet,它的执行函数是 hil_mlcs_process。当程序中调用
tasklet_schedule,会把要执行的结构插入到一个 tasklet 链表,然后触发一个 tasklet 软中断。
每个 CPU 都有自己的 tasklet 链表,内核会根据情况确定在何时执行 tasklet。
可以看到,tasklet 使用起来很简单,本节只需要了解在内核如何使用即可。
1.1.4 工作队列
工作队列和 tasklet 相似,都是一种延缓执行的机制。不同之处是工作队列有自己的进程
上下文,所以工作队列可以睡眠,也可以被调度,而 tasklet 不可睡眠。代码清单 1-3 是工作
队列的例子。
代码清单 1-3 工作队列
INIT_WORK(&ioc->sas_persist_task,
mptsas_persist_clear_table,
(void *)ioc);
schedule_work(&ioc->sas_persist_task);
使用工作队列很简单,schedule_work 把用户定义的 work_struct 加入系统的队列中,并
唤醒系统线程去执行。那么是哪个系统线程执行用户的 work_struct 呢?实际上,内核初始化
的时候,就要创建一个工作队列 keventd_wq,同时为这个工作队列创建内核线程(默认是为
每个 CPU 创建一个内核线程)。
内核同时还提供了 create_workqueue 和 create_singlethread_workqueue 函数,这样用户可
以创建自己的工作队列和执行线程,而不用内核提供的工作队列。看内核的例子:
kblockd_workqueue = create_workqueue("kblockd");
int kblockd_schedule_work(struct work_struct *work){
return queue_work(kblockd_workqueue, work);
}
kblockd_workqueue 是内核通用块层提供的工作队列,需要由 kblockd_workqueue 执
行的工作就要调用 kblockd_schedule_work,其实就是调用 queue_work 把 work 插入到1.1 内核基础层提供的服务 ◆ 5
kblockd_workqueued 的任务链表。
create_singlethread_workqueue 和 create_workqueue 类似,不同之处是,像名字揭示的一
样,create_singlethread_workqueue 只创建一个内核线程,而不是为每个 CPU 创建一个内核
线程。
1.1.5 自旋锁
自旋锁用来在多处理器的环境下保护数据。如果内核发现数据未锁,就获取锁并运行;
如果数据被锁,就一直旋转(其实是一直反复执行一条指令)。之所以说自旋锁用在多处理器
环境,是因为在单处理器环境(非抢占式内核)下,自旋锁其实不起作用。在单处理器抢占
式内核的情况下,自旋锁起到禁止抢占的作用。
因为被自旋锁锁着的进程一直旋转,而不是睡眠,所以自旋锁可以用在中断等禁止睡眠
的场景。自旋锁的使用很简单,请参考下面的代码例子。
spin_lock(shost->host_lock);
shost->host_busy++;
spin_unlock(shost->host_lock);
1.1.6 内核信号量
内核信号量和自旋锁类似,作用也是保护数据。不同之处是,进程获取内核信号量的时
候,如果不能获取,则进程进入睡眠状态。参考代码如下:
down(&dev->sem);
up(&dev->sem);
内核信号量和自旋锁很容易混淆,所以列出两者的不同之处。
?内核信号量不能用在中断处理函数和 tasklet 等不可睡眠的场景。
?深层次的原因是 Linux 内核以进程为单位调度,如果在中断上下文睡眠,中断将不能
被正确处理。
?可睡眠的场景既可使用内核信号量,也可使用自旋锁。自旋锁通常用在轻量级的锁场
景。即锁的时间很短,马上就释放锁的场景。
1.1.7 原子变量
原子变量提供了一种原子的、不可中断的操作。如下所示:
atomic_t mapped;
内核提供了一系列的原子变量操作函数,如下所示。
?atomic_add:加一个整数到原子变量。
?atomic_sub:从原子变量减一个整数。
?atomic_set:设置原子变量的数值。
?atomic_read:读原子变量的数值。6
◆ 第 1 章 内核的基础层和应用层
1.2 内核基础层的数据结构
内核使用的数据结构有双向链表、hash 链表和单向链表,另外,红黑树和基树(radix 树)
也是内核使用的数据结构。实际上,这也是程序代码中通常使用的数据结构。
container是Linux中很重要的一个概念,使用container能实现对象的封装。代码如下所示:
#de?ne container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})
这个方法巧妙地实现了通过结构的一个成员找到整个结构的地址。内核中大量使用了这
个方法。
1.2.1 双向链表
list 是双向链表的一个抽象,它定义在 /include/linux 目录下。首先看看 list 的结构定义:
struct list_head {
struct list_head *next, *prev;
};
list 库提供的 list_entry 使用了 container,通过 container 可以从 list 找到整个数据对象,
这样 list 就成为了一种通用的数据结构:
#de?ne list_entry(ptr, type, member)
container_of(ptr, type, member)
内核定义了很多对 list 结构操作的内联函数和宏。
?LIST_HEAD:定义并初始化一个 list 链表。
?list_add_tail:加一个成员到链表尾。
?list_del:删除一个 list 成员。
?list_empty:检查链表是否为空。
?list_for_each:遍历链表。
?list_for_each_safe:遍历链表,和 list_for_each 的区别是可以删除遍历的成员。
?list_for_each_entry:遍历链表,通过 container 方法返回结构指针。
1.2.2 hash 链表
hash 链表和双向链表 list 很相似,它适用于 hash 表。看一下 hash 链表的头部定义 :
struct hlist_head {
struct hlist_node *?rst;
};
和通常的 list 比较,hlist 只有一个指针,这样就节省了一个指针的内存。如果 hash 表非
常庞大,每个 hash 表头节省一个指针,整个 hash 表节省的内存就很可观了。这就是内核中
专门定义 hash list 的原因。1.2 内核基础层的数据结构 ◆ 7
hash list 库提供的函数和 list 相似,具体如下。
?HLIST_HEAD:定义并初始化一个 hash list 链表头。
?hlist_add_head:加一个成员到 hash 链表头。
?hlist_del:删除一个 hash 链表成员。
?hlist_empty:检查 hash 链表是否为空。
?hlist_for_each:遍历 hash 链表。
?hlist_for_each_safe:遍历 hash 链表,和 hlist_for_each 的区别是可以删除遍历的成员。
?hlist_for_each_entry:遍历 hash 链表,通过 container 方法返回结构指针。
1.2.3 单向链表
内核中没有提供单向链表的定义。但实际上,有多个地方使用了单向链表的概念,看代
码清单 1-4 的例子。
代码清单 1-4 单向链表
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
上面的例子是字符设备的 map 表,probe 结构其实就是单向链表。这种结构在内核中应
用很广泛。
1.2.4 红黑树
红黑树是一种自平衡的二叉树,代码位于 /lib/rbtree.c 文件。实际上,红黑树可以看做折
半查找。我们知道,排序的快速做法是取队列的中间值比较,然后在剩下的队列中再次取中
间值比较,迭代进行,直到找到最合适的数据。红黑树中的“红黑”代表什么意思呢?红黑
的颜色处理是避免树的不平衡。举个例子,如果 1、2、3、4、5 五个数字依次插入一颗红黑
树,那么五个值都落在树的右侧,如果再将 6 插入这颗红黑树,要比较五次。为避免这种情
况,“红黑规则”就要将树旋转,树的根部要落在 3 这个节点,这样就避免了树的不平衡导
致的问题。
内核中的 I/O 调度算法和内存管理中都使用了红黑树。红黑树也有很多介绍的文章,读
者可以结合代码分析一下。
1.2.5 radix 树
内核提供了一个 radix 树库,代码在 /lib/radix-tree.c 文件。radix 树是一种空间换时间的
数据结构,通过空间的冗余减少了时间上的消耗。radix 树的形象图如图 1-1 所示。8
◆ 第 1 章 内核的基础层和应用层
图 1-1 radix 树的形象图
如图 1-1 所示,元素空间总数为 256,但元素个数不固定。如果用数组存储,好处是插
入查找只用一次操作,但是存储空间需要 256,这是不可思议的。如果用链表存储,存储
空间节省了,但是极限情况下查找元素的次数等于元素的个数。而采用一颗高度为 2 的基
树,第一级最多 16 个冗余成员,代表元素前四位的索引,第二级代表元素后四位的索引。
只要两级查找就可以找到特定的元素,而且只有少量的冗余数据。图中假设只有一个元素
10001000,那么只有树的第一级有元素,而且树的第二级只有 1000 这个节点有数据,其他
节点都不必分配空间。这样既可以快速定位查找,也减少了冗余数据。
radix 树很适合稀疏的数据,内核中文件的页缓存就采用了 radix 树。关于 radix 树的文
章很多,读者可以结合内核 radix 树的实现代码分析一下。
1.3 内核应用层
内核应用层是建立在基础层之上的功能性系统。在本书中,内核应用层指的是文件系
统、设备、驱动以及网络。内核代码虽然庞杂,但是核心的基础层并不庞大,主要是应用层
占据了大部分代码量。图 1-2 展示了内核各部分的代码量统计数据。
图 1-2 内核代码的统计数据1.4 从 Linux 内核源码结构纵览内核 ◆ 9
从图 1-2 可以计算得出,驱动、文件系统和网络占据了内核代码的绝大部分,而代表基
础层的 kernel 和内存管理实际上只有很少的代码量。Architectures 属于内核的基础层,它是
为适配不同的 CPU 结构提供了不同的代码,对某种 CPU 来说(如读者最关注的 x86CPU),
实际的代码量也大大减少了。
1.4 从 Linux 内核源码结构纵览内核
本节通过 Linux 内核源码的各个目录来分析内核代码的数量和阅读难度。如图 1-3 所示。
从图 1-3 可以发现,Architectures 的子目录是各个 CPU 架构的名字,为各种不同的 CPU
架构服务。虽然总体量很大,但是对读者关注的 x86 或者 ARM 来说,也只占很小的一部分。
图 1-4 展示了内核中 drivers 目录的分类。
drivers 目录分类为各种不同的设备驱动,而设备驱动虽然五花八门,但是它们的结构是
高度相似的,读者可以根据工作需要阅读分析驱动代码。在理解设备驱动架构的基础上,这
个工作具有高度重复性,可以在短时间内掌握驱动的精髓。
图 1-5 展示了内核中 fs 目录的分类。fs 目录分类为各种不同的文件系统。
图 1-3 内核中 Architectures
目录的分类
图 1-4 内核中 drivers 目录
的分类
图1-5 内核中fs目录的分类
Linux 为文件系统统一提供了一个 VFS 架构,各种文件系统都要按照这个架构来设计。10
◆ 第 1 章 内核的基础层和应用层
因此,各种不同的文件系统都具有重复的部分,读者不需要逐一分析所有的文件系统代码,
只选择几种文件系统重点阅读即可。
1.5 内核学习和应用的四个阶段
如何深入学习内核?或者更进一步,如果把内核知识应用到具体的工作中,对工作产生
价值?
根据所有内核核心开发人员的观点,阅读代码、理解代码是最重要的步骤。对于操作系
统这种庞大复杂的基础软件来说,只学习操作系统教程之类的书籍,远远不能达到理解和应
用的目的。这是所有工程实践类学科的特点,“只在岸上学习是永远不可能掌握游泳技巧的”。
所以必须以代码为依托,以代码为依归。
其次,是如何选择代码的问题。内核代码非常庞大,如何在开始阶段选择代码,既能覆
盖主要方面,同时又不至于难度太高,是分析和学习的主要问题。本书把内核代码分为了基
础层和应用层,对基础层突出内核 API 的概念,在应用层的分析中,希望通过突出重点架构
和选择典型例子来加深理解。为了便于阅读,笔者将简单的代码注释直接加在代码清单里,
需要重点解释的部分,在正文加以说明。
学以致用,任何学习的坚实基础都不只是单纯的兴趣,而是要在实践中得到检验。所以
检验学习效果,要看实际的应用,而不能只是单方面地阅读分析代码。实际的应用和学习过
程,笔者认为,可以粗略地分为四个阶段。
(1)起步阶段
结合中国当前的应用现状,起步阶段基本都是从驱动入手。这一阶段的表现是,实际做
过几个驱动,能够移植驱动到不同的系统平台,对驱动能够做一定的修改,能够裁剪内核,
以适应具体的需求;对 Linux 的 bootloader 能够根据需求做修改。根据笔者对国内现状的了
解和调查,大多数国内的内核应用停留在这个层面,大多数内核相关的工作也是在这个层面
进行。
(2)熟练阶段
对内核的一个或者几个部分比较熟悉,针对熟悉部分,可以进行深度的开发应用。比如
对设备驱动相关的总线、设备、中断比较熟悉,并且可以做深层次的开发。这一阶段的特点
是对内核的理解还不够全面,需要时间积累增加对内核整体的把握。
(3)高级阶段
对整个内核的重要部分都进行了比较深入的分析。这一阶段的特点是全面性,即使要学
习内核某些新的重要特性,也能在短时间内迅速掌握重点。
(4)终极阶段
此阶段是 Linux 内核组维护人员所达到的水准,能做开创性的工作,具有重大的应用价
值。处于这个阶段的主要是欧美的资深开发人员(或者说是内核 hacker),国内达到这个水准
的技术人员非常少。

时间: 2024-10-11 01:15:59

内核的基础层和应用层的相关文章

内核5步下应用层断点

1:使用 !process 0 0 察看所有 EPROCESS 2:使用.process /p + 需要断的应用程序的EProcess地址,切换到应用程序的地址空间 例如: .process  /p  0x80a02a60 3:重新加载user PDB文件 .reload /f /user 4:使用非侵入式的切换进程空间 .process /i /p 0x80a02a60 5:下应用层断点

内核编程学习小结

The road to success was trial and error development, recompilation, and lots of crashes. 寒假过去一个月,计划很多时候也没法跟上.不过总体上来说,还是学习和收获了一些东西的.过去的事情不能改变,所以也不必过于纠结和懊悔.假期的前期还计划对英语进行系统性的学习,我个人任务是画错时间,用错力了,看专业英语的时间完全可以用开翻译与内核编程有关的开发文档,这样更有意义一些,在发现问题后也没办法放下,这是很不好的一个缺

windows内核Api的学习

windows内核api就是ntoskrnl.exe导出的函数.我们可以跟调用应用层的api一样,调用内核api.不过内核api需要注意的是,如果函数导出了,并且函数文档化(也就是可以直接在msdn上搜索到).ExFreePool函数导出,并且文档化,那么我们可以直接调用.导出了未文档化,那么我们就要声明.什么叫文档化和未文档化呢?大家来看一个函数: UCHAR *PsGetProcessImageFileName(IN PEPROCESS Process); 文档化:就是假设函数导出了,并且在

Netfilter/Iptables Layer7 应用层过滤策略部署

Netfilter/Iptables Layer7 应用层过滤策略部署 环境:内核版本:Linux version 2.6.32-431.el6.x86_64 iptables版本:iptables v1.4.7 gcc版本:4.6.1 软件准备:wget http://download.clearfoundation.com/l7-filter/netfilter-layer7-v2.23.tar.gz wget https://www.kernel.org/pub/linux/kernel/

linux内核(三)文件系统

1.为什么需要根文件系统 (1)init进程的应用程序在根文件系统上(2)根文件系统提供了根目录/(3)内核启动后的应用层配置(etc目录)在根文件系统上.几乎可以认为:发行版=内核+rootfs(4)shell命令程序在根文件系统上.譬如ls.cd等命令总结:一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件./bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作. 下面是根文件系统顶层目录 根文件系统的实质是

二.x86内核保护机制--段保护

一.段的作用: 在x86-16体系中,为了解决16位寄存器对20位地址线的寻址问题,引入了分段式内存管理.而段的沿用,一方面是为了保持向下的拓展性,另一方面,也增加了可寻址的范围,增加了CPU的性能. 随着CPU性能的大幅度提升,生产商的研发重点,也开始着重于计算机的稳定性,和数据的安全性,因此,在会影响到计算机稳定性和重要数据的地方,就要给用户加上限制,限制用户的行为主要是,数据的读,写和执行,在限制用户的同时,又不能影响操作系统对数据和代码的使用,因此,引入了层(R0,R1,R2,R3)的概

Nginx为什么比Apache Httpd高效:原理篇

一.进程.线程? 进程是具有一定独立功能的,在计算机中已经运行的程序的实体.在早期系统中(如linux 2.4以前),进程是基本运作单位,在支持线程的系统中(如windows,linux2.6)中,线程才是基本的运作单位,而进程只是线程的容器.程序 本身只是指令.数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例.若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循 序)或异步(平行)的方式独立运行.现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借

TCP的流量控制和拥塞控制

一.首先和UDP作比较谈谈TCP的特点 (1)UDP是数据报协议,每个数据报都有长度,数据报的长度和数据一起发送和接受,各个数据报的发送和接受相互独立,互不影响.而TCP是字节流协议,所有经TCP发送的数据没有记录边界. (2) UDP是无连接.不可靠的协议,而TCP是有连接,可靠协议.TCP的可靠性主要指经TCP发送的数据要么准确无误的发送到了对端,要么发送失败并且以合适的方式通知应用程序,也就是可靠的传输数据和可靠地报告错误,TCP协议每一次发送数据以后先暂时将数据保存在缓存区,直到接收到对

对Devops的思考和构想——建立机器世界的生态系统 (结局篇)

本文参考<失控>著作的部分智慧结晶 本文还参考了"Docke到底解决了什么问题"这篇文章里面的部分智慧结晶 本次系列文章暂定为三篇分节以及最后这篇结局篇,暂时仅完成一篇. 警告:这是一篇仍未完成的文章,由于内容比较难写充实,权且看看有没有人看,再想想是否要继续写下去... 道家说"道生一,一生二,二生三,三生万物",其中的"二"即是阴和阳,它们代表世界的正反两面,阴阳交融而得以衍生万物.那么,现实社会和互联网世界,哪个是阴,哪个是阳呢