1.1 内核的任务
- 内核:硬件与软件之间的中间层,其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址
1.2 实现策略
- 微内核:内核仅实现最基本的功能,所有其它功能委托给独立进程,包括文件系统、内存管理等
- 宏内核:所有子系统都打包到一个文件中,内核中的每个函数可以访问内核中所有其它部分
- Linux:基于宏内核,但是引入了模块可以动态添加/移除功能
1.3 内核组成部分
1.3.1 进程、进程切换、调度
- 进程:操作系统下运行的应用程序、服务器及其它程序
- 地址空间:每个进程在CPU的虚拟内存中分配地址空间,各进程的地址空间是完全独立的(即各个进程不会意识到彼此的存在)
- 进程间通信:使用特定的内核机制(注:IPC机制)
- 进程切换:进程之间的切换过程。撤销进程CPU要保存所有状态,并将进程置于空闲状态;激活进程CPU要将保存的状态原样恢复
- 并发:同一个CPU同一时刻只能运行一个进程。内核按照的时间间隔在不通的进程之间切换,造成同时处理多个任务的假象
- 并行:多CPU系统,真正同时让多个进程执行(不超过CPU数目)
- 调度:确定哪个进程运行多长时间的过程
- 重要进程得到的CPU时间多一点,次要的进程得到的少一点
1.3.2 UNIX进程
- 进程树:Linux对进程采用一种层次系统,每个进程依赖于一个父进程(
pstree查看) - init进程:第一个进程,负责系统的初始化操作,并显示登录提示符或图形登录界面。所有进程都直接或间接源自该进程
- 创建进程:
fork和execfork:创建子进程,并将父进程内存内容复制到子进程。Linux采用写时复制优化fork操作,fork后父进程和子进程只读访问同一内存页exec:将新程序加载到当前进程的内存中并执行
- 线程:又称为轻量级进程,共享主程序地址空间下的数据和资源。进程可以看做是正在执行的程序,线程是与主程序并行运行的程序函数或例程
- 命名空间:传统Linux使用许多全局量,如进程ID。Linux 2.6支持命名空间,使得全局资源有不同分组,每个命名空间可以包含特定的PID集合,或提供文件系统的不同视图。命名空间中不同的进程可以看到不同的系统视图
- 注:容器基于命名空间实现。与完全虚拟化方案(如KVM)相比,计算机只需要运行一个内核来管理所有的容器
1.3.3 地址空间与特权级别
- 虚拟地址空间:每个进程各自都有逻辑地址空间。CPU的字长决定了地址空间的最大范围。32位系统可以管理B的空间;64位系统可以管理B的空间
- 从每个进程的角度来看,地址空间内只能看到自身进程的存在。
- 虚拟地址空间的最大范围与实际物理内存大小无关
- 虚拟地址空间划分为内核空间和用户空间
- 用户空间:
0~TASK_SIZE,用户进程自身虚拟地址范围(IA-32系统TASK_SIZE=3GB,即每个用户进程认为自身有3GB内存) - 内核空间:
TASK_SIZE以上,保留给内核专用,用户进程不能访问
- 用户空间:
- 特权级别:CPU硬件提供了几种特权级别,各级别看作环,内环能访问更多功能
- Linux内核级别:IA-32体系结构提供了4种特权级别,Linux只使用两种不同的状态:内核态和用户态。用户态禁止访问内核空间,用户进程不能操作或读取内核的数据和代码
- 系统调用:普通进程要使用影响整个系统的操作(如操作输入/输出设备),需要用户态到内核态的转换。通过系统调用的手段完成转换。
- 用户进程借助系统调用向内核发出请求
- 内核检查进程是否允许该操作
- 内核代表进程执行所需的操作
- 由内核态返回到用户态
- 中断:内核还可以由异步硬件中断激活,然后在中断上下文中运行
- 由于中断是随机发生的,当前用户进程与中断的原因无关,因此中断发生情况下,内核无权访问当前用户空间的内容
-
内核线程:内核中运行的应用程序,无权处理用户空间。例如:用户内存和块设备之间的数据同步、帮助调度器在CPU分配进程等(
ps fax可以看到:用方括号括起来的为内核线程) -
虚拟地址和物理地址:每个进程有自身的虚拟地址空间。大多数情况下,虚拟地址空间比实际可用的物理内存大。内核使用页表的数据结构,将虚拟地址空间映射到物理地址空间,巧妙地解决了1GB可用内存的物理机实际可以跑4GB的应用程序的问题
- 页:虚拟地址空间和物理内存被内核划分为很多等长部分。页一般专指虚拟地址空间的页
- 页帧:物理内存页常称作页帧
- 注意:两个页可能共享同一个页帧。内核可决定哪些内存区域在进程之间可以共享,哪些不能共享
1.3.4 页表
- 页表:虚拟地址空间映射到物理地址空间的数据结构,最容易的方案是使用数组实现。
- 多级页表:IA-32体系结构使用4KB页,则映射4GB的虚拟地址空间需要项的数组,内存占用太多。因此将虚拟地址分为四部分,使用三级页表进行映射。
- 好处:对虚拟地址空间中不需要的区域,不需要创建中间页目录或页表,节省大量内存
- 缺点:需要访问多级数组才能转换为物理地址(时间换空间)
MMU(Memory Management Unit,内存管理单元):CPU专门部分,可以优化内存访问操作TLB:地址转换中最频繁的那些地址,缓存到TLB的CPU高速缓存中。可直接访问TLB获取物理地址
1.3.5 物理内存的分配
内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域 内核可以只分配完整页帧,其它更小部分的分配工作可委托给用户空间的标准库完成
1. 伙伴系统
系统长期运行时,频繁的分配和释放页帧可能会导致:系统中若干页帧是空闲的,但是散落在物理地址空间各处(缺乏连续页帧组成的较大内存块)。分配连续页框的应用难以满足
Linux内核引入伙伴系统算法(buddy system),某种程度上减少了内存碎片(但无法完全消除)
- 系统的空闲内存块总是两两分组的,每组的两个内存块称为伙伴。伙伴的分配是彼此独立的
- 所有空闲页框默认分组为11个块链表,每个块链表分别包含1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。应用程序最大可以申请1024个连续页框(对应4MB大小的连续内存)
- 块申请:从块链表查找空闲块。如果没找到,向更上级的链表依次查找内存块:内存块分裂为两部分,一块分配给应用,另一块加入块链表中。如果实在没有,返回错误
- 块释放:内核将伙伴合并为更大的内存块放回到伙伴列表中,即内存块分裂的逆过程
【例】书中例子
- 初始系统中有16个页帧(64KB)的内存块
- 应用程序需要8个页帧的内存(32KB),将16个页帧的内存块拆成两个伙伴,一块满足应用程序,另一块放置到8页大小的块链表中
- 应用程序需要2个页帧的内存(8KB),8页的内存块分裂为2个伙伴,每个伙伴包含4页帧。一块放到4页大小的块链表中,另一块继续分裂为2个伙伴:一块放到2页大小的块链表中,另一块分配给应用程序
2. slab缓存
内核经常需要分配比完整页帧小得多的内存块,定义了一个额外的slab分配器,将伙伴系统提供的页划分为更小的部分。
- 块申请:从slab缓存快速分配(使用后释放到缓存)。缓存用尽时向伙伴系统请求新的页帧
- 块释放:块申请逆过程
【总结】页帧的分配有伙伴系统进行,slab分配器负责分配小内存以及提供一般性的内核缓存
3. 页面交换和页面回收
- 页面交换:本质是通过硬盘空间扩展内存大小,不经常使用的页可以置换到硬盘中。如果需要访问相关内存,内核通过缺页异常机制,将硬盘数据置换到内存中,恢复用户进程执行。此过程对用户进程不可见
- 页面回收:将内存映射被修改的内容与底层块设备同步,也简称为数据回写。之后页帧可用于其它用途
1.3.6 计时
jiffies:内核使用jiffies的时间坐标测量时间及不同时间点的时差- 通常是由定时器中断按恒定的时间间隔递增名为
jiffies_64和jiffies的全局变量 jiffies的递增频率与体系结构有关,取决于内核的常数Hz(通常介于100到1000之间,粒度较粗,当然可以用高分辨率定时器实现更精确的计时)- 在没有或无需频繁的周期性操作的情况下,周期性地产生定时器中断是没有意义的,可以动态降低计时周期。这对供电受限的系统很有用
- 通常是由定时器中断按恒定的时间间隔递增名为
1.3.7 系统调用
- 系统调用:用户进程从用户态切换到内核态,并将系统关键任务委派给内核执行,必须经过系统调用
- 进程管理:创建新锦成,查询信息,调试
- 信号:发送信号,定时器以及相关处理机制
- 文件:创建、打开和关闭文件,从文件读取和向文件写入,查询信息和状态
- 目录和文件系统:创建、删除和重命名目录,查询信息,链接,变更目录
- 保护机制:读取和变更UID/GID,命名空间的处理
- 定时器函数:定时器函数和统计信息
1.3.8 设备驱动程序、块设备和字符设备
UNIX一切皆文件,对外设的访问可以利用/dev目录下的设备文件夹完成
- 字符设备:提供连续的数据流,应用程序支持按字节/字符顺序读写数据,不支持随机存取(如:调制解调器)
- 块设备:应用程序可以随机读写设备数据,但不支持基于字符的寻址(如:硬盘)
1.3.9 网络
Linux使用了源于BSD的套接字抽象支持通过文件接口处理网络连接
- 发送数据:内核根据各个协议层的要求,打包数据,然后发送
- 接收数据:内核针对各个协议层的处理,对数据进行拆包和分析,然后将有效数据传递给应用程序
1.3.10 文件系统
- 文件系统:Linux系统由许多文件组成,数据存储在块设备上。文件系统使用目录结构组织存储的数据,并将元数据与实际数据关联。Linux支持许多文件系统:ext2和ext3、ReiserFS、XFS、VFAT等
- inode:每个文件的管理结构,包含了文件所有的元信息,以及指向相关数据块的指针
- 目录:表示为普通文件,其数据包含了目录下所有文件的inode指针
- 虚拟文件系统(VFS):所有文件系统必须实现的接口层,将应用层和具体文件系统隔离开
1.3.11 模块和热插拔
- 模块:动态地向内核添加和卸载功能,如设备驱动程序、文件系统、网络协议等
- 本质也是普通程序,只是在内核空间执行
- 模块必须提供
init.text和exit.text向内核注册和注销模块 - 模块代码的权限与普通内核代码相同,可以访问内核中所有的函数和数据
- 好处:使内核支持种类繁多的设备,而内核自身的大小不会膨胀
- 热插拔:系统检测到新设备时,通过加载对应的模块,将必要的驱动程序自动添加到内核中(如USB和FireWire),无需系统重启
1.3.12 缓存
内核使用了大量的缓存改进系统性能。应用程序访问数据时,可以从内存中读取,绕过了低速的块设备(本质是内存IO比磁盘IO快)
- 页缓存:内核是基于页的内存映射访问块设备的,因此缓存按页组织,把整页缓存起来,称为页缓存
- 块缓存:缓存没有组织成页的数据,重要性差得多(目前已经被页缓存取代)
1.3.13 链表处理
内核建立了一个双向循环链表,第一个和最后一个元素都能达到O(1)的访问时间
- 注意:链表的实现不是类型安全的,需要显示指定类型
- 链表数据结构必须包含
list_head的链表元素,包含正向和反向指针
struct task_struct {
...
struct list_head run_list;
...
};
struct list_head {
struct list_head *next, *prev;
};
1.3.14 对象管理和引用计数
- 对象管理:2.5内核提供了一般性方法管理内核对象,防止代码复制,也为不同部分的对象提供了一致的视图
1. 一般性的内核对象
kobject嵌入其他数据结构中,作为内核对象的基础kref:用于管理引用计数,其类型是原子数据类型atomic_t。当计数器为0时,则不需要该对象,可从内存删除- 与C++和Java的面向对象思想不谋而合,提供了内核面向对象技术的可能性
struct sample {
...
struct kobject kobj;
...
};
struct kobject {
const char * k_name;
struct kref kref;
struct list_head entry;
struct kobject * parent;
struct kset * kset;
struct kobj_type * ktype;
struct sysfs_dirent * sd;
};
struct kref {
atomic_t refcount;
};
2. 对象集合
- 对象集合
kset:目的是将不同的内核对象归类到集合中(如所有字符设备的集合、所有基于PCI设备的集合)- 本身是内核对象
kobject,包含所有属于当前对象的链表list kobj_type描述内核对象的共同特性
- 本身是内核对象
struct kset {
struct kobj_type *ktype;
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
struct kset_uevent_ops *uevent_ops;
};
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops * sysfs_ops;
struct attribute ** default_attrs;
};
3. 引用计数 引用计数:用于检测内核有多少个地方使用某个对象。使用则+1;不使用-1;=0释放
atomic_t是原子的,在多处理器系统也是安全的(详见第5章分析)
struct kref {
atomic_t refcount;
};