深入Linux内核架构:第一章 简介和概述

1.1 内核的任务

在纯技术层面上,内核是硬件与软件之间的一个中间层。其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。

1.2 实现策略

操作系统实现主要有两种形式:
(1) 微内核:这种范型中,只有最基本的功能直接由中央内核(即微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与中心内核通信。例如,独立进程可能负责实现各种文件系统、内存管理等。
(2) 宏内核:与微内核相反,宏内核是构建系统内核的传统方法。在这种方法中,内核的全部代码,包括所有子系统(如内存管理、文件系统、设备驱动程序)都打包到一个文件中。内核中的每个函数都可以访问内核中所有其他部分。

1.3 内核的组成

1.3.1 进程相关

Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,该进程负责进一步的系统初始化操作,因此init是进程树的根,所有进程都直接或间接起源自该进程。

  • 创建进程
    fork可以创建当前进程的一个副本,在该系统调用执行之后,系统中有两个进程,都执行同样的操作(使用写时复制技术)。一般而言,使用fork后会接着使用exec将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,其内容将替换为新的数据,然后开始执行新程序。init就是通过这种方式创建其他进程的。
  • 创建线程
    Linux内核中其实是将线程当作进程来看待的,使用clone方法创建线程(fork底层也是调用clone),但是创建的时候启用了精确的检查,以确认哪些资源与父进程共享、哪些资源为线程独立创建。
  • 进程调度
    由于Linux是多任务系统,它支持并发执行的若干进程。系统中同时真正在运行的进程数目最多不超过CPU数目,因此内核会按照短的时间间隔在不同的进程之间切换。Linux通过调度策略决定使用哪种调度器,调度器可以决定一个进程运行多久以及下一个运行哪个进程。

1.3.2 地址空间

CPU的字长决定所能管理的地址空间的最大长度,对于32位的系统是2^32 B,对于64位的系统就是2^64 B(对于64位系统而言,一般是只使用42位或47位,实际可寻址范围小于理论长度)。地址空间的最大长度与实际可用的物理内存数量无关。Linux将虚拟地址空间划分为内核空间和用户空间,用户进程是无法直接访问内核空间的。
从用户状态到核心态的切换通过系统调用的特定转换手段完成,且系统调用的执行因具体系统而
不同。如果普通进程想要执行任何影响整个系统的操作,则只能借助于系统调用向内核发出请求。内核首先检查进程是否允许执行想要的操作,然后代表进程执行所需的操作,再返回到用户状态。

1.3.3 内存相关

(1)页表
页表是一种将虚拟地址空间映射到物理地址空间的数据结构,如图所示,进程A的虚拟内存页1
映射到物理内存页4,而进程B的虚拟内存页1映射到物理内存页5。而两个虚拟地址空间中的页(虽然在不同的位置)也可以映射到同一物理内存页。由于内核负责将虚拟地址空间映射到物理地址空间,因此可以决定哪些内存区域在进程之间共享,哪些不共享。
在这里插入图片描述Linux采用了多级分页的模型,将虚拟地址划分为多个部分,例如下图中将虚拟地址划分为4部分,这样需要一个三级的页表,而Linux采用的四级页表。

  • 全局页目录(Page Global Directory,PGD)
    虚拟地址的第一部分称为PGD。PGD用于索引进程中的一个数组(每个进程有且仅有一个),该数指向另一些数组的起始地址,这些数组称为中间页目录。
  • 中间页目录(Page Middle Directory,PMD)
    虚拟地址中的第二个部分称为PMD,在通过PGD中的数组项找到对应的PMD之后,则使用PMD
    来索引。PMD的数组项也是指针,指向下一级数组,称为页表或页目录。
  • PTE(Page Table Entry,页表数组)
    虚拟地址的第三个部分称为PTE(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的。
  • 偏移量
    虚拟地址最后的一部分称为偏移量。它指定了页内部的一个字节位置。归根结底,每个地址都指
    向地址空间中唯一定义的某个字节。

多级页表的一个特色在于,对虚拟地址空间中不需要的区域,不必创建中间页目录或页表,可以节省大量内存。
然而如果每次访问内存都要经过多级查询,效率必定会很低,因此CPU使用了一些方法来进行加速。一种是MMU(Memory Management Unit,内存管理单元),优化了内存访问操作。另一种是将地址转换中出现最频繁的那些地址,保存到称为地址转换后备缓冲器(Translation Lookaside Buffer,TLB)的CPU高速缓存中,无需访问内存中的页表即可从高速缓存直接获得地址数据。

(2)物理内存的分配
在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由
于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。
- 伙伴系统
内核中很多时候要求分配连续页,为快速检测内存中的连续区域,内核采用了伙伴系统。系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴。
内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理。
各有8页的一对伙伴也在相应的列表中。
分配:如果系统现在需要8个页帧,则将16个页帧组成的块拆分为两个伙伴。其中一块用于满足应用程序的请求,而剩余的8个页帧则放置到对应8页大小内存块的列表中。
回收:在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放回到伙伴列表中,这刚好是内存块分裂的逆过程。
这种分配方式在长期运行时会产生内存碎片问题,即大量空闲的页帧散布在物理地址空间各处。
- slab缓存
在伙伴系统的基础上为频繁使用的小对象实现了一个一般性的缓存:
对于频繁使用的对象,内核定义了只包含所需类型对象实例的缓存,每次需要这种类型的对象时,直接分配,slab缓存自动维护与伙伴系统的交互;
对于通常的小对象,内核使用了一组slab缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。例如:kmalloc和kfree。

(3)页面交换与回收
页面交换通过利用磁盘空间作为扩展内存,从而增大了可用的内存。在内核需要更多内存时,不经常使用的页可以写入硬盘。如果再需要访问相关数据,内核会将相应的页切换回内存。通过缺页异
常机制,这种切换操作对应用程序是透明的。换出的页可以通过特别的页表项标识。在进程试图访问
此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存
中。接下来用户进程可以恢复运行。由于进程无法感知到缺页异常,所以页的换入和换出对进程是完
全不可见的。
页面回收用于将内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写。数据
刷出后,内核即可将页帧用于其他用途(类似于页面交换)。内核的数据结构包含了与此相关的所有
信息,当再次需要该数据时,可根据相关信息从硬盘找到相应的数据并加载。

1.3.4 计时器

内核必须能够测量时间以及不同时间点的时差,jiffies_64和jiffies(分别是64位和32位)的全局变量,会按恒定的时间间隔递增。递增的频率同体系结构有关,取决于内核中一个主要的常数HZ。计时的周期是可以动态改变的,对于供电受限的系统是很有用的,例如笔记本电脑和嵌入式系统。

1.3.5 系统调用

系统调用是用户进程与内核交互的经典方法。POSIX标准定义了许多系统调用,以及这些系统调
用在所有遵从POSIX的系统包括Linux上的语义。系统调用函数对用户空间是不可见的,在发出系统调用时,处理器必须改变特权级别,从用户状态切换到核心态。

1.3.6 设备驱动程序、块设备和字符设备

设备驱动程序用于与系统连接的输入/输出装置通信,如硬盘、软驱、各种接口、声卡等。对外设的访问可利用/dev目录下的设备文件来完成,程序对设备的处理完全类似于常规的文件。设备驱动程序的任务在于支持应用程序经由设备文件与设备通信。换言之,使得能够按适当的方式在设备上读取/写入数据。
(1) 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类
设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。
(2) 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的
块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常
是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

1.3.7 网络

网卡也可以通过设备驱动程序控制,但网卡不能利用设备文件访问,在网络通信期间,数据打包到了各种协议层中。
在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,然后才能将有效数据传递给应用程序。
在发送数据时,内核必须首先根据各个协议层的要求打包数据,然后才能发送。

1.3.8 文件系统

Linux支持许多不同的文件系统,因此内核必须提供一个额外的软件层,将各种底层文件系统的具体特性与应用层(和内核自身)隔离开来。该软件层称为VFS(Virtual Filesystem或Virtual Filesystem Switch,虚拟文件系统或虚拟文件系统交换器)。VFS既是向下的接口(所有文件系统都必须实现该接口),同时也是向上的接口。
在这里插入图片描述

1.3.9 模块和热插拔

模块用于在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等,模块还可以在运行时从内核卸载,这在开发新的内核组件时很有用。模块在本质上不过是普通的程序,只是在内核空间而不是用户空间执行而已。模块必须提供某些代码段在模块初始化(和终止)时执行,以便向内核注册和注销模块。
对支持热插拔而言,模块在本质上是必需的。某些总线(例如,USB和FireWire)允许在系统运
行时连接设备,而无需系统重启。在系统检测到新设备时,通过加载对应的模块,可以将必要的驱动
程序自动添加到内核中。
模块特性使得内核可以支持种类繁多的设备,而内核自身的大小却不会发生膨胀。在检测到连接
的硬件后,只需要加载必要的模块,多余的驱动程序无需加入到内核。

1.3.10 链表处理

内核提供的标准链表可用于将任何类型的数据结构彼此链接起来,加入链表的数据结构必须包含一个类型为list_head的成员,其中包含了正向和反向指针。

struct list_head {
	struct list_head *next, *prev;
};

// 使用方法是将list_head嵌入数据结构的属性中,例如task_struct
struct task_struct {
...
	struct list_head run_list;
...
};

作为数据结构的成员,则struct list_head被称作链表元素。用作链表起点的元素被称作
表头。表头同样是list_head的实例,通常用LIST_HEAD(list_name)宏来声明并初始化。
对于链表的操作,内核提供了很多函数,例如list_add(), list_del(), list_for_each()等。

1.3.11 对象和引用计数

内核采用了一般性的方法来管理内核对象,一般性的内核对象机制可用于执行下列对象操作:

  • 引用计数;
  • 管理对象链表(集合);
  • 集合加锁;
  • 将对象属性导出到用户空间(通过sysfs文件系统)。
    一般性的内核对象,包括以下结构
// <kref.h>
struct kref {
	atomic_t refcount;		// 原子类型,用于引用计数
};

// <kobject.h>
struct kset {
	struct kobj_types		*ktype;			// 指向kset中各个内核对象公用的kobj_type结构
	struct list_head		list;			// 内核对象链表结构
	struct kobject			kobj;			// 管理kset对象本身
	struct kset_uevent_ops	*uevent_ops;	// 提供若干函数指针,用于将集合状态传递给用户层。
};

// 该结构提供了与sysfs文件系统的接口
struct kobj_type {
	struct sysfs_ops	*sysfs_ops;
	struct attribute	**default_attrs;
};

/*
kobject不是通过指针与其他数据结构连接起来,而必须直接嵌入。这样做,通过管
理kobject即达到了对包含kobject对象的管理。由于kobject结构会嵌入到内核的许多数
据结构中,开发者需要注意保持该结构较小。向该数据结构添加一个新成员,则会导致许
多其他数据结构的大小增加
*/
struct kobject {
	const char			   *k_name;		// 对象的文本名称,可用sysfs导出到用户空间
	struct kref  	 	    kref;		// 简化引用计数的管理
	struct list_head        entry;		// 内嵌的链表元素
	struct kobject 	  		*parent;	// 指向父对象的指针
	struct kset 		    *kset;		// 将对象与其他对象放置到一个集合时,需要kset 
	struct kobj_type  		*ktype;		// 提供包含kobject的结构数据的更多详细信息
	struct sysfs_dirent  	*sd;		// 支持内核对象与sysfs之间的关联
};

1.3.12 数据类型

(1)类型定义
内核使用typedef来定义各种数据类型,以避免依赖于体系结构相关的特性,例如内核定义了若干整数数据类型,不仅明确标明了是有符号数还是无符号数,而且还指定了相关类型的精确位数。__s8和__u8分别是有符号和无符号的8位整数。
(2)字节序
在大端序格式中,最高有效字节存储在最低地址,而随着地址升高,字节的权重降低。在小端序格式中,最低有效字节存储在最低地址,而随着地址升高,字节的权重也升高。
在这里插入图片描述
内核提供了各种函数和宏,可以在CPU使用的格式与特定的表示法之间转换。cpu_to_le64将64
位数据类型转换为小端序格式,而le64_to_cpu所做的刚好相反。
(3)per-cpu变量
通过DEFINE_PER_CPU(name, type)声明,其中name是变量名,而type是其数据类型,在有若干CPU的SMP系统上,会为每个CPU分别创建变量的一个实例。用于某个特定CPU的实例可以通过get_cpu(name, cpu)获得,其中smp_processor_id()可以返回当前活动处理器的ID,用作前述的cpu参数。
(4)用户空间
源代码中的多处指针都标记为__user,该标识符对用户空间程序设计是未知的。内核使用该记
号来标识指向用户地址空间中区域的指针,在没有进一步预防措施的情况下,不能轻易访问这些指针
指向的区域。这是因为内存是通过页表映射到虚拟地址空间的用户空间部分的,而不是由物理内存直
接映射的。因此内核需要确保指针所指向的页帧确实存在于物理内存中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值