《深入Linux内核架构》读书笔记第一章简介和概述

第一章 简介和概述

1.1 内核的任务

内核是硬件和软件的中间层,其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
应用程序与硬件没有联系,只与内核有联系,所以内核是应用程序所知道的层次结构中的最底层。

1.2 实现策略

在系统运行时,模块可以插入到内核代码中,也可以移除。这样就可以向内核动态地添加功能。
模块特性依赖于内核与用户层之间设计精巧的通信方法,这使得模块的热插拔和动态装载得以实现。

1.3 内核的组成部分

1.3.2 UNIX进程

UNIX操作系统有2种创建新进程的机制,分别是fork和exec.
(1) Linux使用了“写时复制”的技术(copy on wirte)来使得fork的操作更加高效,主要原理是:将内存复制操作延迟到父进程或子进程向某内存页面写入数据之前,在只读访问的情况下父进程和子进程共用同一内存页。
(2) exec将一个新程序加载到当前的进程的内存中并执行。旧程序的内存页将刷出,其内容将替换为新的数据,然后开始执行程序。

使用命名空间(namespace)之后,每个命名空间可以包含一个特定的PID的集合,或提供文件系统的不同视图,在某个命名空间中挂载的volume不会传播到其他的命名空间中。
并非内核的所有部分都完全支持命名空间。内核的各个子系统对namespace的支持程度不同。

1.3.3 地址空间和特权级别

Linux将虚拟地址空间划分为2个部分,分别称为内核空间和用户空间。
系统中每个用户进程都有自身的虚拟地址范围,从0到TASK_SIZE. 用户空间之上的区域(从TASK_SIZE到2^32或2^64)保留给内核专用,用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常熟,把地址空间按给定比例划分为2个部分。例如,在IA-32系统中,地址空间在3GB处划分,因此每个进程的虚拟地址空间是3GB,而由于虚拟地址空间总长度为4GB,所以内核空间有1GB可用。
由于地址空间虚拟化的结果,每个用户进程都认为自身有3GB内存。各个系统进程的用户空间是完全彼此分离的。
而对于64位的计算机,在实际管理巨大的理论虚拟地址空间时,倾向于使用小于64位的位数,比如42位或47位。因此地址空间中实际可寻址的部分小于理论长度。这种做法的一个优点是,因为管理有效地址空间所需位数较少,CPU可以节省一些工作量。

  1. 特权级别
    所有现代CPu都提供了集中特权级别,进程可以驻留在某一特权级别。

从用户态到内核态的切换,通过系统调用完成:由系统调用向内核发出请求,内核首先检查是否允许进程执行想要做的操作,然后代表进程执行所需操作,接下来返回到用户态。

除了代表用户程序执行代码外,内核还可以由异步硬件中断激活,然后在中断上下文中运行。与在进程上下文中运行的主要区别是,在中断上下文中运行是不能访问虚拟地址空间中的用户空间部分的。

除了普通进程,系统中还有内核线程在运行。内核线程不与任何特定的用户空间进程相关联,因此也无权处理用户空间。不过在其他许多方面,内核线程更像普通的用户层应用程序。与在中断上下文中运行的内核相比,内核线程可以进入睡眠状态,也可以像普通进程一样被调度器跟踪。
内核线程的用途:内存和块设备之间的数据同步,以及帮助调度器在CPU上分配进程。
注意,在ps命令的输出中很容易识别内核线程,其名称都置于方括号内。
在多处理器系统上,许多线程启动时指定了CPU,并限制只能在某个特定的CPU上运行。从内核线程名称之后的斜线和CPU编号可以看出这一点。
PID TTY STAT TIME COMMAND
100 ? S< 0:00 [events/3]

  1. 虚拟和物理地址空间

物理内存页经常称作页帧,而虚拟地址空间中的页就称为页。

名词:
用户层(userland):指的是应用程序本身;而BSD社区更喜欢用该术语称呼所有不属于内核的东西。
用户空间:不仅指应用程序,还指代了应用程序所运行的虚拟地址空间的一部分,与内核空间相对。

1.3.4 页表

页表:将虚拟地址空间映射到物理地址空间的数据结构。

多级分页:
PGD(全局页目录,Page Global Directory):其数组项指向另一些数组(PMD)的起始地址;
PMD(中间页目录,Page Middle Directory):其数组项指向下一级数组PTE(称为页表或页目录);
PTE(页表数组,Page Table Entry):用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的。
OFFSET(偏移量):指定页内部的某个字节位置。

对虚拟地址空间中不需要的区域,不必创建中间页目录(PMD)或页表(PTE),因此多级页表节省了大量内存。

从虚拟地址到页帧需要多级转换,CPU试图用下面2种方法加速此过程:
1. CPU中有一个专门的部分,叫做MMU(内存管理单元),该单元优化了内存访问操作;
2. 地址转换中出现最频繁的哪些地址,保存到称为 TLB(地址转换后备缓冲器,Translation Lookasside Buffer)的CPU高速缓存中。无须访问内存中的页表,即可从CPU高速缓存直接获得地址数据。

内存映射:

可以将任意来源的数据传输到进程的虚拟地址空间中。
比如,文件的内容可以映射到内存中,只需访问相应的内存即可访问文件内容,而向内存写入数据来修改文件内容,内核将保证任何修改都会自动同步到文件中。
内核在实现设备驱动程序时直接使用了内存映射。外设的输入/输出可以映射到虚拟地址空间的区域中。对相关内存区域的读写会由系统重定向到设备,因而大大简化了驱动程序的实现。

1.3.5 物理内存的分配

  1. 伙伴系统
    在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放回到伙伴列表中。这刚好是内存块分裂的逆过程。

  2. slab缓存
    内核本身经常需要比完整页帧小得多的内存块。内核必须在伙伴系统的基础上自定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。该方法不仅可以分配内存,还为频繁使用的小对象实现了一个一般性的缓存 - slab缓存。它可以用2种方法分配内存。
    (1) 对频繁使用的对象,内核定义了各种对象类型,在每次需要某种类型的对象时,可以从对应的缓存快速分配。
    (2) 对通常情况下小内存块的分配,内核针对不同大小的对象定义了一组slab缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。不同之处是这些函数都增加了前缀k,表明是与内核相关联的: kmalloc 和 kfree.

  3. 页面交换和页面回收
    在内核需要更多的内存时,不经常使用的页可以写入磁盘。换出的页可以通过特别的页表项标识。
    在进程试图访问此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时,内核可以将磁盘上的数据切换到内存中。接下来用户进程可以恢复运行。

页面回收用于将内存映射被修改的内容与底层的块设备同步,为此也称为“数据回写”。数据刷出(flush)后,内核即可将页帧用于其他用途(类似于页面交换)。内核的数据结构包含了鱼刺相关的所有信息,当再次需要该数据时,可根据相关信息从磁盘找到相应的数据并加载。

1.3.6 计时

全局变量jiffies_64和jiffies,会按恒定的时间间隔递增。
计算机底层体系结构的执行周期性操作的手段,通常是定时器中断。对上述的2个全局变量的更新可使用底层体系结构提供的各种定时器机制执行。
内核可使用高分辨率的定时器提供额外的计时手段,能够以纳秒级的精确度和分辨率来计量时间。

1.3.7 系统调用

系统调用是用户进程和内核交互的经典方法。
传统的系统调用大致有如下的分类:进程管理、信号、文件、目录和文件系统、保护机制(读取和变更UID/GID,命名空间的处理)、定时器函数。

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

按照“万物皆文件”(everything is a file),对外设的访问可利用/dev目录下的设备文件来完成,程序对设备的处理完全类似于常规文件。

外设分为如下2类:
(1)字符设备:提供连续的数据流。应用程序可以顺序读取,通常不支持随机存取。比如,调制解调器是典型的字符设备。
(2)块设备:应用程序可以随机访问设备数据。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。块设备不支持基于字符的寻址,而字符设备支持。

编写块设备的驱动程序比字符设备复杂得多,因为内核为提高系统性能广泛地使用了缓存机制。

1.3.9 网络

网卡也可以通过设备驱动程序控制,但在内核中属于特例,因为网卡不能利用设备文件访问。原因是,在网络通信期间,数据包发送到了各个协议层中;在接收数据时,内核必须针对各个协议层的处理,解析数据包后,才能将有效载荷传递给应用程序;而发送数据时,内核首先要根据各个协议打包数据,然后才能发送。

1.3.10 文件系统

文件系统使用目录结构组织存储的数据,并将其他元信息(例如所有者、访问权限等)与实际数据关联起来。
Linux支持的文件系统有:Ext2、Ext3、ReiserFS、XFS、VFAT等等。
Ext2基于inode,即它为每个文件都构造了一个单独的管理结构,称为inode,并存储到磁盘上。inode包含了文件所有的metadata(元信息),以及指向相关数据块的指针。目录可以表示为普通文件,其数据包括了指向目录下所有文件的inode的指针,因而层次结构得以建立。

内核必须提供一个额外的软件层,将“各种底层文件系统的具体特性与应用层”和“内核自身”隔离开来。该软件层称为VFS(虚拟文件系统)。

1.3.11 模块和热插拔

模块可以在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等。实际上,内核的任何子系统几乎都可以模块化。
模块还可以在运行时从内核卸载。

模块在本质上仍是普通程序,只是在内核空间而不是用户空间运行而已。模块必须提供一些代码段在模块初始化和终止时执行,以便向内核注册和注销模块。模块代码和普通内核代码的权利和义务都是相同的,可以像编译到内核中的代码一样,访问内核中所有的函数和数据。

对于支持热插拔而言,模块在本质上是必需的:
某些总线(如USB和FireWire)允许在系统运行时连接设备而无需重启系统。在系统检测到新设备时,通过加载对应的模块,可以将必要的驱动程序自动添加到内核中。

内核社区中一个长期存在的争论是,是否应该只为模块提供二进制代码,而不提供源代码。商业公司认为只要提供二进制代码就可以了,但内核本身却是开源的。
目前可以将只提供二进制代码的模块加载到内核,但有很多限制。最重要的一点是,有一些函数是GPL许可的函数,那么就等于是明确规定调用者也必须是GPL许可的,所以对于此类函数,只提供二进制代码的模块就无法访问。

1.3.12 缓存

由于内核是通过基于页的内存映射来实现访问块设备的,因此缓存也按页组织,也就是说,整页都缓存起来,故称为页缓存(page cache)。

块缓存用于缓存没有组织成页的数据,其重要性差得多。很久以前用块缓存,而现在块缓存已经被页缓存取代了。

1.3.13 链表处理

常用双向链表:

struct list_head {
    ...
    struct list_head run_list;
    ...
};

1.3.14 对象管理和引用计数

  1. 一般性的内核对象
    下面的数据结构将嵌入其他数据结构中,用作内核对象的基础:

1.3.15 数据类型

  1. 类型定义

内核使用typedef来定义各种数据类型,以避免依赖于体系结构相关的特性。

内核定义了若干整数数据类型,不仅明确标明了是有符号数还是无符号数,而且还指定了相关类型的精确位数。比如,__s8 和 __u8 分别是有符号和无符号的8位整数。__u16, __s16, __u32, __s32, __u64, __s64 的定义类似。

  1. 字节序

内核提供了各种函数和宏,可以在CPU使用的格式与特定的表示法之间转换。
cpu_to_le64 将64位数据类型转换为小端序格式,而 le64_to_cpu 所做的刚好相反。

  1. per-cpu变量

普通的用户空间程序设计不会涉及的一种特殊变量就是per-cpu变量。它们是通过DEFINE_PER_CPU(name, type)声明,其中name是变量名,而type是数据类型。在有若干CPU的SMP系统上,会为每个CPU分别创建变量的一个实例。用于某个特定CPU的实例可以通过get_cpu(name, cpu)获得,其中smp_processor_id()可以返回当前活动处理器的ID,用作前述的cpu参数。

  1. 访问用户空间

源代码中多处指针都标记为__user,该标识符对用户空间程序设计是未知的。
内核使用该记号来标识指向用户地址空间中区域的指针,在没有进一步预防措施的情况下,不能轻易访问这些指针指向的区域。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值