【深入Linux内核架构笔记】第一章 简介和概述

348 阅读13分钟

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进程:第一个进程,负责系统的初始化操作,并显示登录提示符或图形登录界面。所有进程都直接或间接源自该进程
  • 创建进程:forkexec
    • fork:创建子进程,并将父进程内存内容复制到子进程。Linux采用写时复制优化fork操作,fork后父进程和子进程只读访问同一内存页
    • exec:将新程序加载到当前进程的内存中并执行
  • 线程:又称为轻量级进程,共享主程序地址空间下的数据和资源。进程可以看做是正在执行的程序,线程是与主程序并行运行的程序函数或例程
  • 命名空间:传统Linux使用许多全局量,如进程ID。Linux 2.6支持命名空间,使得全局资源有不同分组,每个命名空间可以包含特定的PID集合,或提供文件系统的不同视图。命名空间中不同的进程可以看到不同的系统视图
    • 注:容器基于命名空间实现。与完全虚拟化方案(如KVM)相比,计算机只需要运行一个内核来管理所有的容器

1.3.3 地址空间与特权级别

  • 虚拟地址空间:每个进程各自都有逻辑地址空间。CPU的字长决定了地址空间的最大范围。32位系统可以管理2322^{32}B的空间;64位系统可以管理2642^{64}B的空间
    • 从每个进程的角度来看,地址空间内只能看到自身进程的存在。
    • 虚拟地址空间的最大范围与实际物理内存大小无关
  • 虚拟地址空间划分为内核空间用户空间
    • 用户空间:0~TASK_SIZE,用户进程自身虚拟地址范围(IA-32系统TASK_SIZE=3GB,即每个用户进程认为自身有3GB内存)
    • 内核空间:TASK_SIZE以上,保留给内核专用,用户进程不能访问

image.png

  • 特权级别:CPU硬件提供了几种特权级别,各级别看作环,内环能访问更多功能

image.png

  • Linux内核级别:IA-32体系结构提供了4种特权级别,Linux只使用两种不同的状态:内核态用户态。用户态禁止访问内核空间,用户进程不能操作或读取内核的数据和代码
  • 系统调用:普通进程要使用影响整个系统的操作(如操作输入/输出设备),需要用户态到内核态的转换。通过系统调用的手段完成转换。
    • 用户进程借助系统调用向内核发出请求
    • 内核检查进程是否允许该操作
    • 内核代表进程执行所需的操作
    • 由内核态返回到用户态
  • 中断:内核还可以由异步硬件中断激活,然后在中断上下文中运行
    • 由于中断是随机发生的,当前用户进程与中断的原因无关,因此中断发生情况下,内核无权访问当前用户空间的内容

image.png

  • 内核线程:内核中运行的应用程序,无权处理用户空间。例如:用户内存和块设备之间的数据同步、帮助调度器在CPU分配进程等(ps fax可以看到:用方括号括起来的为内核线程)

  • 虚拟地址和物理地址:每个进程有自身的虚拟地址空间。大多数情况下,虚拟地址空间比实际可用的物理内存大。内核使用页表的数据结构,将虚拟地址空间映射到物理地址空间,巧妙地解决了1GB可用内存的物理机实际可以跑4GB的应用程序的问题

    • 页:虚拟地址空间和物理内存被内核划分为很多等长部分。页一般专指虚拟地址空间的页
    • 页帧:物理内存页常称作页帧
    • 注意:两个页可能共享同一个页帧。内核可决定哪些内存区域在进程之间可以共享,哪些不能共享

image.png

1.3.4 页表

  • 页表:虚拟地址空间映射到物理地址空间的数据结构,最容易的方案是使用数组实现。
  • 多级页表:IA-32体系结构使用4KB页,则映射4GB的虚拟地址空间需要2202^{20}项的数组,内存占用太多。因此将虚拟地址分为四部分,使用三级页表进行映射。
    • 好处:对虚拟地址空间中不需要的区域,不需要创建中间页目录或页表,节省大量内存
    • 缺点:需要访问多级数组才能转换为物理地址(时间换空间)
  • MMU(Memory Management Unit,内存管理单元):CPU专门部分,可以优化内存访问操作
  • TLB:地址转换中最频繁的那些地址,缓存到TLB的CPU高速缓存中。可直接访问TLB获取物理地址

image.png

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页大小的块链表中,另一块分配给应用程序

image.png

2. slab缓存
内核经常需要分配比完整页帧小得多的内存块,定义了一个额外的slab分配器,将伙伴系统提供的页划分为更小的部分。

  • 块申请:从slab缓存快速分配(使用后释放到缓存)。缓存用尽时向伙伴系统请求新的页帧
  • 块释放:块申请逆过程

【总结】页帧的分配有伙伴系统进行,slab分配器负责分配小内存以及提供一般性的内核缓存

image.png

3. 页面交换和页面回收

  • 页面交换:本质是通过硬盘空间扩展内存大小,不经常使用的页可以置换到硬盘中。如果需要访问相关内存,内核通过缺页异常机制,将硬盘数据置换到内存中,恢复用户进程执行。此过程对用户进程不可见
  • 页面回收:将内存映射被修改的内容与底层块设备同步,也简称为数据回写。之后页帧可用于其它用途

1.3.6 计时

  • jiffies:内核使用jiffies的时间坐标测量时间及不同时间点的时差
    • 通常是由定时器中断按恒定的时间间隔递增名为jiffies_64jiffies的全局变量
    • 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):所有文件系统必须实现的接口层,将应用层和具体文件系统隔离开

image.png

1.3.11 模块和热插拔

  • 模块:动态地向内核添加和卸载功能,如设备驱动程序、文件系统、网络协议等
    • 本质也是普通程序,只是在内核空间执行
    • 模块必须提供init.textexit.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;
};