目录
1.1 内核的任务
1.2 实现策略
1.3 内核的组成部分
编辑1.3.1 进程、进程切换、调度
1.3.2 UNIX 进程
1.3.3 地址空间和特权级别
1.3.4 页表
1.3.5 物理内存的分配
1.3.6 计时
1.3.7 系统调用
1.3.8 设备驱动程序
1.3.9 网络
1.3.10 文件系统
1.3.11 模块和热插拔
1.3.12 缓存
1.3.13 链表处理
1.3.14 对象管理和引用计数
1.3.15 数据类型
1.3.16 本书的局限性
1.4 为什么内核是特别的
1.5 行文注记
1.6 小结
本章是概述,如有不懂,欢迎阅读后续章节。
1.1 内核的任务
管理系统软硬件资源。
1.2 实现策略
常见操作系统有两类实现:
微内核:中央内核+子模块(文件系统,内存管理,网络)
优点:动态拓展性。
缺点:模块间通信负荷大。
宏内核:单内核,所有功能在一个内核镜像中。
优点:模块间通信简单高效,因为都在同一个地址空间。
Linux为宏内核,但引入微内核的模块特性。
1.3 内核的组成部分
1.3.1 进程、进程切换、调度
1.3.2 UNIX 进程
init:第一个运行进程,由内核启动。
pstree命令:查看进程关系拓扑。
Unix系统中创建新进程机制:fork和exec
Linux独有创建新进程机制:clone
fork:采用写时拷贝, 内存复制操作延迟到子进程或父进程写内存。若只读,父子进程共享同一内存页。
命名空间namespace:
内核2.6开始引入,用于支持容器虚拟化,和KVM的虚拟化机制不同。
页帧:物理内存页。
页: 虚拟内存地址空间页。
内核可决定物理内存哪些区域可被多个应用进程共享,哪些区域不共享。
1.3.3 地址空间和特权级别
CPU字长:
含义:CPU一次能并行处理的二进制位数。
决定了可管理地址空间的最大长度。
由task_struct可知:内核中"task"指用户空间进程。
所以TASK_SIZE指用户空间。
32位系统为例:
TASK_SIZE = 3G,即每个进程的用户空间0-3G。
3G-4G内核空间是所有进程共享的,每创建一个进程,就创建一个内核栈(THREAD_SIZE),大小为8或16KB。
所有进程的内核栈,都存在于3G-4G虚拟地址空间中。
传统X86 CPU有4种特权级别:最高ring 0、ring 1、ring 2、ring 3最低。
但Linux设计之初只使用两种:
ring 0:内核态
ring 3:用户态
后来Linux虚拟化需要比ring 0更高的特权级别,如KVM+QEMU中,KVM和QEMU分别都需要不同的特权级别。
所以X86 CPU新增4个特权级别,即root mode的ring0-ring3,原来的ring0-ring3变为non-root mode的ring0-ring3。
如下图:
1.3.4 页表
页表:一个保存了虚拟地址空间到物理地址空间映射关系的数据结构。
页帧:一个物理内存页。
页帧号:该物理页编号。
一个进程的整个虚拟地址空间通常不会同时映射到物理内存。
如:
1. 需要使用时才换入内存时,建立映射表。
2. 内存不足时换出到硬盘交换分区。
虚拟地址空间中未使用或未映射的区域,不必创建页目录和页表,可节省内存。
页表:为减少页表占用内存,采用多级页表。
全局页表(第一级页表)的物理地址放到页表基址寄存器PTR中:
X86中的PTR:CR3寄存器。
ARM-v7:协处理器CP15的寄存器TTBR。
ARM-v8:系统寄存器TTBR。
TTBR:Tranlation Table Base Register
CR3:Control Register 3
四级页表:使用四个数组,而PGD,PMD,PTE,Offset值都作为对应数组的索引。
MMU:Memory Management Unit,CPU内部的一个单元。
功能:
完成虚拟地址到物理地址的转换。
缓存页表。
MMU内包含两部分:
TLB:(Translation Lookaside Buffer),用于缓存页表。
TWU:(Table Walk Unit),执行查询页表。
TLB未命中:没有缓存到虚拟地址对应的物理地址映射,此时才进行四级页表转换。
当页表更改,需使TLB内容无效。
VA:虚拟地址。
PA:物理地址。
1.3.5 物理内存的分配
通过查看页表项中的PG_xx标志。可知对应页帧是已分配或空闲。
内核可按整页分配内存。
也可按字节分配,如kmalloc。
伙伴系统:
目标:
用于分配连续页。
解决内存碎片问题。
当应用程序释放内存时,若相邻内存块都空闲则合并成更大页,并放回到伙伴列表中。
分配粒度:页。
slab缓存
将从伙伴系统分配的页划分为更小的部分 ,用于缓存频繁使用的数据结构,如:task_struct,mm_struct
kmalloc也基于slab机制,对应slab数据结构是kmalloc-32,kmalloc-16等。
slab缓存自动维护与伙伴系统的交互,在缓存用尽时从伙伴系统请求新的页帧。
页面交换swap
内存不足时,将页内容交换到磁盘空间。
被换出的页的页表项有特殊标识:PG_swapcache。
进程访问该标志的页帧时,CPU发出缺页异常,然后内核从硬盘读数据到内存。
1.3.6 计时
每次定时器中断会更新内核全局变量jiffies。
jiffies:系统从启动以来的时钟滴答数(tick)。
HZ常量:每秒时钟滴答次数,即每秒jiffies会增加HZ数。
如系统启动5秒后:jiffies = HZ * 5。HZ值通常为100、250、1000。
1.3.7 系统调用
系统调用原理:
1. 使用int 0x80或syscall指令,触发系统调用中断。
2. 执行中断服务程序ISR。
3. 从EAX寄存器中读取系统调用号。
4. ISR执行系统调用号对应处理函数。
5. 通过寄存器或栈传递函数返回值给用户空间。
6. 用户程序恢复执行。
系统调用号是有限的,所有可能多个系统调用一个号,如socket,bind,connect等都会调用SYSTEM_socket,具体通过参数区分。
x86常用的寄存器EAX、EBX、ECX、EDX
0x80号中断对应的中断处理程序是system_call。
1.3.8 设备驱动程序
字符设备:
连续数据流,按字节为单位读写。
只能顺序读取,不支持随机存取。
块设备:
可随机存取某区域,不需要按顺序。
只能以块(通常512B)为单位读写。
通常有缓存机制。
1.3.9 网络
网卡不能通过设备文件访问。
1.3.10 文件系统
inode:存储文件属性。包含:
文件类型:普通文件、目录、符号链接等。
文件权限:读、写、执行。
文件大小。
文件的访问和修改时间。
文件数据的物理磁盘位置。
Ext2特点:
inode也存储在磁盘上,目录也表示为普通文件。
(inode:存储文件属性。)
一个目录文件的内容是:该目录下所有文件的inode指针。
1.3.11 模块和热插拔
热插拔原理:
1. 内核检测热插拔。
2. 内核调用modprobe/hotplug等应用程序。
3. 加载相应驱动或子系统。
1.3.12 缓存
块设备的缓存管理:
分配内存页面。
映射块设备数据:将块设备的数据映射到分配的页面上。
缓存管理:当需要访问块设备时,先检查缓存中是否存在所需的数据。如果存在,直接访问缓存中数据,而无需访问实际的块设备。
数据同步:修改块设备数据后,将数据从缓存写回到块设备上,确保数据同步。
1.3.13 链表处理
struct list_head {
struct list_head *next, *prev;
};
表头struct list_head *p;
void list_add(struct list_head *new, struct list_head *head) //从表头插入
{
__list_add(new, head, head->next);
}
void list_add_tail(struct list_head *new, struct list_head *head)//从表尾插入
{
__list_add(new, head->prev, head);
}
void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
next->prev = new;
prev->next = new;
}
int list_empty(struct list_head *head)
{
return head->next == head;
}
1.3.14 对象管理和引用计数
kset kobj ktype作用:
用于设备模型。
构建内核中的对象系统。
总结:
kobject:
表示内核中的各种实体。
kset:
表示内核对象的集合,可组织和管理这些对象。
可有一个或多个子 kset,形成父子关系的树形结构。
ktype:
定义了 kobj对象的属性、方法。如提供/sys中对象对应的读写函数。
struct kobject {
const char *name;
struct list_head entry; 用于将当前对象链接到父对象中。
struct kobject *parent; 父对象
struct kset *kset; 所属kset
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref; 引用计数
};
kobject相关函数:
kobject_get,kobject_put,kobject_register,kobject_add
struct kset { //作用:归类
struct list_head list; 连接该kset集合所有kobject
spinlock_t list_lock;
struct kobject kobj; 该kset所有kobject的父对象。
struct kset_uevent_ops *uevent_ops; 生成uevent事件给hotplug进程,实现热插拔功能。
}
struct kobj_type { //即ktype,作用:提供sysfs文件系统接口
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
引用计数kref值封装在一个结构中,以防止直接+/-操作该值。必须使用对应API,如:
kref_get(struct kref *kref);
kref_put();
1.3.15 数据类型
pid_t,sector_t,struct kref: 这些变量不能直接访问,需通过指定函数。
MIPS CPU可同时支持大小端,需设置。
cpu_to_le64:将64位数据转成小端。
不存在cpu_to_le8函数,因为8位数据(char)大小端都一样。如下图:
判断大小端的代码:
int num = 0x11223344;
int isBigEnd()
{
int *p = #
char *p2 = (char*)p;
if (*p2==0x11){
return 1;
}
return 0;
}
per-cpu变量:
多处理器中为每个CPU都分配一个值。数据在各自CPU缓存中,可快速访问不同CPU缓存中,避免同步加锁。
定义方法:
DEFINE_PER_CPU(type, name)
读写变量值:
int value = get_cpu_var(per_cpu_var);
//读写操作...
put_cpu_var(per_cpu_var);
__user:表示该指针属于用户地址空间。
1.3.16 本书的局限性
1.4 为什么内核是特别的
许多体系不支持非对齐的内存访问。