Linux内核体系结构
5.1 Linux 内核模式
目前的操作系统内核模式主要可以分为两种模式:
- 整体式的单内核式 Monolithic Kernel
- 层次式的微内核式 Microkernel Designs
《linux kernel development》第一章里面特别对于monolithic kenel 和microkernel 进行了对比。下面是我LKD第一章的笔记。有目录,很快可以翻到
http://blog.csdn.net/cinmyheart/article/details/23769725#t3
在单内核模式的系统中(eg.linux),操作系统所提供的服务流程:
应用程序使用指定的参数值执行系统调用指令(int 0x80),使用CPU从user mode 切换到kernel mode,然后操作系统根据具体的参数值调用特定的服务程序,而这些服务程序则根据需要调用底层的一些支持函数以完成特定的功能。
monolithic kernel的三个层次:
- 调用服务的主程序
- 执行系统调用的服务层
- 支持系统调用的底层函数
5.2 linux 内核系统体系结构
linux内核主要由5个模块构成
文件系统模块用于支持外部设备的驱动和储存。虚拟文件系统模块通过向所有的外部储存设备提供一个通用的文件接口,隐藏了各种硬件设备的不同细节。从而提供并支持与其他操作系统兼容的多种文件格式。
所有的模块都与进程调度模块存在依赖关系。因为它们都需要依靠进程调度程序来挂起(暂停)或重新运行它们的进程。通常一个模块会在等待硬件操作期间被挂起,而在操作完成后才可继续运行。
5.3 Linux 内核对内存的管理和使用
5.3.1 物理内存
5.3.2 内存地址空间概念
Vitual address
虚拟地址 是指有程序产生的由段选择符和段内偏移地址两部分组成的地址。因为这两部分组成的地址并没有直接用来访问物理内存,而是通过分段地址变换机制处理或者映射之后才对应到物理地址上,因此这种地址被称为虚拟地址。虚拟地址空间由GDT(global descriptor table)映射的全局地址空间和由LDT(local descriptor table)映射的局部地址空间组成。
logical address
逻辑地址 是指由程序产生的与段相关的偏移地址部分。在Intel保护模式下即是指程序执行代码段限长内的偏移地址(假定代,码段数据段完全一样)。
linear address
线性地址 是虚拟地址到物理地址之间变化的中间层。是处理器可寻址的内存空间中的地址。若没有启用分页机制,那么线性地址直接就是物理地址。
5.3.3内存分段机制
我觉得把段选符贴一下比较好。。。
有图有真相。
"I very rarely think in words at all. A thought comes, and I may try to express it in words afterwards."
-Albert Einstein, 1916
而在保护模式运行下,段寄存器中存放的不再是被寻址段的基地址,而是一个段描述符表(segment descriptor table)中某一描述符项中的索引值。索引值指定的段描述符项中含有需要寻址的内存段的基地址,段的长度值和段的访问权限级别等信息。
对于中断描述符表IDT(interrupt descriptor table) 它保存在内核代码中。由于Linux0.12内核中,内核和各任务的代码段和数据段都被分别映射到线性地址空间中相同基地址处,且段限长也一样,因此内核的代码段和数据段是重叠的。
5.3.4 内存分页管理
我觉得《注释》这本书的x86讲保护模式编程的时候讲分页管理有点。。。臭。。。上次我就懒得看分页管理了,有点逛过去的意思,只是认真看了分段机制。还是这里“补习”了一下,感觉还行。。。
使用分页机制最普遍的场合就是当系统内存实际上被分成很多凌乱的块时,它建立一个大而连续的内存空间映像,好让程序不用操心和管理这些分散的内存块。
内存分页管理机制的基本原理就是将CPU整个线性内存区域划分为4KB为1页的内存页面。程序申请内存的时候,系统就以内存页为单位进行分配。
页目录表和页表项格式基本相同,都占用4字节,并且每个页目录表或页表必须只能包含1024个页表项。因此一个页目录表或者一个页表分别占用1页内存。页目录项和页表项的小区别在于页表项有个已写位D(dirty),而页目录项没有。
由于Linux 0.1x 系统中内核和所有任务都共用一个页目录表,使得任何时刻处理器线性地址空间到物理地址空间的映射函数都一样。因此为了让内核所有任务都不互相干扰和重叠。他们都必须从虚拟地址空间映射到线性地址空间的不同位置,即占用不同的线性地址空间范围。
由于linux0.12 把每个进程最大可用虚拟内存空间定义为64M,因此每个进程的逻辑地址通过加上(任务号)*64M,就可以转换成线性地址空间。
5.3.5 CPU多任务和保护方式
内核代码和数据是由所有任务共享的,因此它们保存在全局地址空间中。
当以个任务执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(简称内核态)。此时处理器处于最高的0级特权级内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个程序都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时。此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与内核态的进程的状态有些相似。
5.3.6虚拟地址,线性地址和物理地址之间的关系
1.内核代码和数据的地址
对于内核代码来说,虚拟地址,线性地址和物理地址三者的关系如图
可以看出
1. 内核代码段和数据段区域在线性地址空间和物理地址空间中是一样的。这样设置可以大大简化内核的初始化操作。
2.GDT和IDT在内核数据段中,因此它们的线性地址也同样等于它们的物理地址。进入保护模式之后,物理地址从0x00一直到0x90200都被挪用做高速缓冲,因此此时需要重新设置GDT和IDT
3.除了任务0以外,所有其他任务使用的物理内存页面与线性地址中的页面至少有部分不同,因此内核需要动态的在主存区中为它们做映射操作,动态的建立页目录表。
2.任务0的地址对应的关系
3.任务1地址对应的关系
在任务2被创建出来之后,执行execve()函数时,系统虽然在线性地址空间为任务2分配了64M的空间范围,但是内核并不会立刻为其分配和映射物理内存页面。只有当任务2执行时由于发生缺页而引起的异常时才会由内存管理程序为其在主内存中分配并映射一页物理内存到其线性地址空间中。这种分配和映射物理内存页面的方法叫做需求加载(load on demand)
5.5 系统调用
5.5.1 系统调用接口
系统调用(syscalls) 接口是linux内核与上层应用程序进行交互通信的唯一接口。
5.5.2 系统调用过程
当应用程序经过库函数向内核发出一个中断调用int 0x80时,就开始执行一个系统调用。其中寄存器eax中存放着系统调用号,而携带的参数可依次存放在寄存器ebx,ecx和edx中。在linux 0.12中最多向内核传递3个参数,当然也可以不带参数。当参数超过三个时,那么内核采用的方法是把这些参数作为一个参数缓冲块,并把这个缓冲块的指针作为一个参数传递给内核。
5.7Linux进程控制
5.7.1 任务数据结构
5.7.2进程运行状态
5.7.4 创建新进程
fork之后,系统并不会为新进程分配实际的物理内存页面,而是让它共享父进程的内存页面,只有当父进程或新进程中任意一个有写内存的时候,系统才会为执行写操作的进程分配相关独自使用的内存页面。这种处理方式称为写时复制(COW,copy on write)。
注意,创建一个新的子进程和加载运行一个执行程序文件是两个不同的概念。当创建子进程时,它完全赋值了父进程的代码和数据区,并会在其中执行子进程部分的代码。而执行块设备上的程序时,一般是子进程中运行exec()系统调用来操作的。进入exec()之后子进程原来的代码和数据区就会被清理掉(释放)。待该子进程开始运行新程序时,由于内核还有从块设备上加载该程序的代码,CPU会立刻产生代码页面不存在的异常(fualt),此时内存管理就会从块设备上加载相应的代码页面,然后CPU重新执行引起异常的指令。到此时新程序的代码才开始真正执行。
5.7.5 进程调度
0.12是非抢占式内核调度,抢占式的用户进程调度。
1. 调度程序
shedule()函数首先扫描任务数组。通过比较每个就绪状态(TASK_RUNNING)任务的运行时间递减滴答计数器counter的值,来确定当前哪个进程运行的时间最少。哪个的值最大,就表示时间还不长,于是就选中该进程,并使用任务切换宏函数,切换到该进程运行。
如果此时所有处于TASK_RUNNING状态进程的时间片都已经用完,系统就会根据每个进程的优先权值prority。对系统中所有进程重新计算每个任务需要运行的时间片值counter:
counter = (counter/2) + priority
这样,在睡眠的进程被唤醒时就具有较高的时间片counter值。
只要系统空闲,就调用进程0。
2.进程切换
每当选择出一个新的可运行进程时,shedule()函数就会调用定义在在include/asm/system.h 中的switch_to()宏执行实际进程切换操作。该宏会把CPU的当前进程状态替换成新进程的状态。在进行切换之前,switch_to()首先检查要切换到的进程是否就是当前进程,如果不是则什么也不做,直接退出。否则,就首先把内核全局变量current置为新任务指针。
5.7.6 终止进程
当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit(),该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开的所有文件,对进程使用的当前工作目录,根目录和运行程序的i节点进行同步操作。如果进程有子进程,则让init进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,并向属于该会话的所有进程发送挂断信号SIGHUP,这通常会终止该会话中的所有进程。
5.8 Linux 系统中堆栈的使用方法
Linux 0.12使用了4中堆栈。
1. 引导初始化时临界使用的堆栈。
2. 进入保护模式之后提供内核程序初始化使用的堆栈,位于内核代码地址空间固定位置处。该堆栈也是后来任务 0使用的任务用户态堆栈。
3.每个任务通过系统调用,执行内核程序时使用的堆栈,我们称之为任务的内核堆栈,每个任务都有自己独立的 内核态堆栈。
4.任务在用户态执行的堆栈,位于任务逻辑地址空间近末端处。
5.8.1 初始化阶段
在init/main.c 程序中,在执行move_to_user_mode()代码把控制权移交给任务0之前,系统一直使用上述堆栈。
5.8.2 任务的堆栈
每个任务都有两个堆栈,分别用于用户态和内核态程序的执行。
(1)在用户态
(2)在内核态运行时
(3)任务0和任务1的堆栈
任务0(空闲进程idle)和任务1(初始化进程init)的代码段和数据段相同,限长也是640KB,但是它们被映射到不同的线性地址范围中。任务0的段基地址从线性地址0开始,而任务1的段基地址从64MB开始。但是它们全都映射到物理地址0~640KB 范围中。这个地址范围也就是内核代码和基本数据所存放的地方。