【Linux内核】---- 02 从main到怠速
发布日期:2021-06-29 14:51:06 浏览次数:3 分类:技术文章

本文共 9300 字,大约阅读时间需要 31 分钟。

【Linux内核】---- 02 从main到怠速

系统达到怠速状态前所做的一切准备工作的核心目的是让用户进程能够以“进程” 的方式正常运行,

能够实现 用户程序能够在主机上进行运算、能够与外设进行交互、以及能够让用户以它为媒介进行人机交互。

系统大体分为三个阶段:

  • 第一阶段:创建进程0,并让进程0具备在32位保护模式下在主机运算的能力
  • 第二阶段:以进程0为母本创建进程1,使进程1不仅仅具备程0 所拥有的能力,而且还能以文件的形式与外设进行数据交互
  • 第三阶段:以进程1为母本创建进程2,使进程2在全面具备进程1所拥有的能力 和 环境的基础上,进一步具备支持“人机交互”的能力,最终实现怠速。

2.1 开中断之前的准备工作

2.1.1 复制根设备号 和 硬盘参数表

在后续进行缓冲区初始化时,会对“根设备号” 和 “硬盘参数表” 重新规划,

具体执行代码如下:

void main(void){
ROOT_DEV = ORIG_ROOT_DEV; // 系统用 ROOT_DEV 备份 “根设备号” ,用以从软盘上回载根文件系统 struct drive_info {
char dummy[32];} drive_info; // 用 drive_info 备份硬盘参数表,大小为32字节 // 每个硬盘参数表为16字节,所以drive_info能够将两个硬盘参数表全部备份。}

2.1.2 规划物理内存

进程0 在主机中的运算主要是通过CPU 和内存相互配合工作而得以实现的,因此,对主机物理内存的使用及管理进进行规划,这样在才能为进程0 具备运算能力打下基础。

具体操作如下:

除1MB 以内的内核区之外,其余物理内存要完成的工作是不同,
“主内存区” 主要用来承载进程的相关信息,包括进程管结构、进程对应的程序等 。
“缓冲区” 主要作为主机与外设进行数据并互的中转站;
“虚拟盘区” 是一个可选的区域,如果选择使用虚拟盘,就可以将外设上的数据先复制到虚拟盘区,然后再使用。

由于内存中操作数据的速度远高于外设,因此这样可以提高系统执行效率。

先根据内存条大小对“缓冲区” 和 “主内存区” 的位置 和 大小进行初步设定。

// 代码: //int/main.cvoid main(void){
memory_end = (1<<20) + (EXT_MEM_K<<10); // 主内存区的末端 memory_end &= 0xfffff000; if(memory_end > 16 * 1024 * 1024) memory_end = 16*1024*1024; if(memory_end > 16 * 1024 * 1024) buffer_memory_end = 4*1024*1024; // 缓冲区的末端位置 else if(memory_end > 6 * 1024 * 1024) buffer_memory_end= 2*1024*1024; else buffer_memory_end= 1*1024*1024; main_memory_start = buffer_memory_end; //主内存区的起始位置}

2.1.3 虚拟盘设置与初始化

虚拟盘大小设置为2MB,表现为在主内存起始处为虚拟盘开辟一段2MB的内存空间

// init/main.cvoid main(void){
#ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK *1024); #endif }// kernel/ramdisk.clong rd_init(long mem_start, int length){
blk_dev[MAJOR_NR].request_fn = DEVICE_RQUEST; for(i = 0; i

2.1.3 内存管理结构mem_map 初始化

对主内存区起始位置的重新确定,标志着主内存区和缓冲区的位置和大小已全部确定了。

于是系统开始调用mem_init 函数,先对主内存区的管理结构进行置。

// init/main.cvoid main(void){
mem_init(main_memory_start, memory_end);}// mm/memory.cvoid mem_init(long start_mem, long end_mem){
for(i = 0; i
0) mem_map[i++] = 0;}

系统对1MB 以上的内存都是分页管理的,于是系统通过一个叫做mem_map的数组记录每一个页面的使用次数。

先将所有的内存页面使用次数均设置为100, 然后再依据主内存的起始位置 和 终止位置将处于主内存中所有的页面的使用次数全部清0,
系统以后只把使用次数为0的页面视为空闲页面。

2.1.4 异常处理类中断服务程序挂接

由于CPU在运算过程中免不了进行“异常处理” ,这些异常处理都需要具体的服务程序来执行。

此时,调用trap_init 函数 将“ 异常处理 ” 一类的中断服务程序 与中断描述符表进行挂接,开始逐步地重建中断服务休系,以此来支持进程0在主机中的运算

“异常处理” 就是由CPU 探测到的一些不可预知的错误而导致的中断,具体包括: 除0 错误、溢出操作、边界检查错误、缺页错误等。

进入trap_init 函数后,系统主要通过调用 set_trap_gate 这样的宏函数来实现挂接,具体操作如下:

set_trap_gate(0 , &divide_error);
参数中的0 表示将该函数地址挂接在中断描述符表的第0项位置年,&divide_error 代表了“除0错误”处理函数的地址。

另外,挂接工作除了要将服务程序的地址值载入描述符表外,还要对每个与之建立关系的描述符进行设置。

每个中断描述符为8个字节,挂接的时伟会将中断处理程序的地址值按照一定的规则进行拆分,然后分散存储在这8个字节中,
同时还要将一些针对本中断的属性信息记录在这8个字节的其余空间中,比如,中断的优先权值等 。

其他异常处理服务程序的挂接方式与“除0 错误” 服务程序的挂接方式大体一致,32位保护模式下的中断服务体系就是通过不断地建立这种挂接关系形成的。

这种32位中断服务体系是为适应一种被动响应中断信号的机制而建立的,它的执行路线如下:

一方面,硬件产生信号,并传达给8259A,8259A 对信号进行初步处理,然后视CPU执行情况传递中断信号。
另一方面,如果CPU没有收到信号,就不断地处理正在执行的程序;如果接收到了信号,就打断正在执行的程序并通过中断描述符表找到具体的中断服务程序,让其执行,执行完后,从刚才打断的程序点继续执行。可见CPU 采取的是一套“被动响应”的机制来处理中断,这样CPU就可以把全部精力都放在为用户程序服务上,对于随时可产生而又不可能时时都产生的中断信号,不用刻意去考虑,这就提高了操作系统的综合效率。

最原始的设计其实不是这样的,那时候CPU每隔一段时间就要对所有硬件进行轮询,以检测它刚才工作的进程中有没有产生信号,这样就分散了CPU处理用户程序的精力,从而降低了系统的综合效率。不仅如此,每一个硬件在一次轮询的过程中产生多少信号也不确定,所以这个硬件还需要存储自身产 生的信号,这就提 高了硬 件设计要求。

可见,以“被动响应” 模式 取代 “主动轮询” 模式 来处理中断问题是现代操作系统之所以被称为“现代” 的一个重要标志。

2.1.5 初始化块设备请求项结构

2.1.6 与建立人机交互界面相关的外设的中断服务程序挂接

  1. 对串行口进行设置
  2. 对显示器进行设置
  3. 对键盘进行设置

2.1.7 开机启动时间设置

进程0 在主机中正常运算需要具体对时间的掌控能力,

2.1.8 系统开始激活进程0

  1. 系统先要将进程0 激活,使其具备运算及创建其他进程的能力
    进程0管理结构task_struct 的母本已经在代码设计阶段事先设计好了,但这并不代表进程0已经可用了
  2. Linux 0.11 作为一个现代操作系统,最重要的标志是能够支持多进程轮流执行,因此需具备参与多进程轮询的能力。
  3. 进程0要具备处理系统调用的能力。

2.1.9 进程相关事务初始化设置

2.1.10 时钟中断设置

时钟中断是进程0及其他由它创建的进程轮询的基础,对时钟中断设置分为如下三个步骤:

  1. 对支持轮询的8253定时器进行设置。
  2. 对与轮询相关的服务程序进行设置。
  3. 将8259A芯片中与时钟中断相关的屏蔽码打开。

打开后,时钟中断就可以产生了,从现在开始,时种中断每1/100秒就产生一次。由于此时处于“关中断”状态,

所以这些产生的信号并不响应,但进程0已经具备参与进程轮询的能力。

2.1.11 系统调用服务程序挂接

将系统调用处理函数set_system_gate 与中断描述符表相挂接。

system_call 是整个操作系统中系统调用软中断的总入口,所有用户程序产生了系统调用软中断后,系统都是通过这个总入口进一步找到具体的系统调用函数的。

系统调用函数是操作系统对用户程序最基本的支持,在操作系统中,为了对内核进行保护,是不允许用 户进程直接访内核的,但处理具体事务又需要内核代码的支持(如读写文件),因此系统提供一个方案—系统调用软中断,即提供一套系统服务接口,用户进程只要想和内核打交道,就调用这套接口程序,之后就会引发软中断,后面的事情就不需要用户程序负责了,而是通过另一条执行路线— 由CPU对这个信号进行响应,通过中断描述符表找到系统调用端口,进而调用到的系统调用函数来处理事务。

可见,所有的系统调用函数,其实出是中断函数,也是软中断服务程序。

2.1.12 初始化缓冲区管理结构

缓冲区是内存与外设(块设备)进行数据交互的媒介。

内存与外设最大的区别在于:

外设 的作用仅仅就是对数据信息以逻辑块的形式进行断电保存,并不参与运算(因为CPU无法到硬盘上进行寻址); 而 内存 除了需要对数据进行保存以外,还要通过与CPU和总线配合,进行数据运算; 缓冲 区则介于两者之间,它既能够对数据信息进行保存,也能够直接参与运算。

有了缓冲区后,

对于外设而言,它仅需要考虑与缓冲区进行数据交互是否符合要求,而不需要考虑内存如何使用这些交互的数据。
对于内存而言,它也仅需要考虑与缓冲区交互的条件是否成熟,而不需要考虑此时外对缓冲区的交互情况。
它们两者的组织、管理和协调将由操作系统统一进行,这样就大大降低了数据处理的维护成本。

系统分别通过空闲表和哈希表这两套数据结构对缓冲区进行管理,所以要分别对两套数据结构进行设置。先对空闲表进行设置,使之成为一个“双环链表”,其中的成员,比如对应的设备号,引用次数,更新标志,脏标志,锁写标志等均被调置为0.

2.1.13 初始化硬盘

  1. 将硬盘请求项服务程序do_hd_request 与请求项函数控制结构blk_dev[7] 的第3 项相挂接(hd.c中 #define MAJOR_NR 3)
  2. 将硬盘中断服务程序hd_interrupt 与中断描述符表IDT 的第 0x2E项直挂接 。
  3. 复位硬盘的中断请求屏蔽位,充许硬盘控制器发送中断请求信号。

2.1.14 初始化软盘

  1. 将软盘请求项服务程序do_fd_request 与请求项函数控制结构blk_dev[7] 的第2 项相挂接(floppy.c中 #define MAJOR_NR 2)
  2. 将软盘中断服务程序floppy_interrupt 与中断描述符表IDT 的第 0x26项直挂接 。
  3. 复位软盘的中断请求屏蔽位,充许软盘控制器发送中断请求信号。

2.1.15 开中断

现在系统中断服务程序都已经和中断描述符表正常挂接了,这意味着中断服备体系已经构建完毕,系统可以在32位保接模式下处理中断信号了,所以要将中断开启

2.2 进程创建的最基本动作

2.2.1 操作系统为进程 0 创建进程1做准备

在Linux 0.11 中,除进程0 外,所有进程都是由一个已有进程在用户态下完成创建的。

为了遵守这个规则,在进程0 正式创建 进程1之前,要将进程0 由内核态转变为用户态,方法是调用move_to_user_mode函数,模仿中断返回运作,实现进程0 的特权级从内核态转变为用户态。

在执行完 move_to_user_mode()后,相当于进行了一次中断返回,这就会导致CS 特权级的值 从0 转换为3 ,即从内核态转换成为用户态。

在进程0 正式运行,调用 fork 函数,开始创建进程1,所有用户进程 在创建新进程的时候都 要调用这个函数。

2.2.2 在进程槽中为进程1申请一个空闲位置并获取进程号

  1. 在激活进程0的过程中,已经对其所在的进程槽task[64] 进行了清空设置,创建 出了空闲项,这些空闲项在这里就要发挥作用了。
  2. 先调用 find_empty_process 函数,为这个进程1获取一个可用的进程号 和 一个空闲的任务号。
  3. 内核中使用全局变量last_pid 来存放系统自开机以来累计的进程数,也将此变量用作新建进程的进程号。在内核的数据区中有一个task[NR_TASKS] , 也就是进程槽,此数组有64项,用来存放进程的 task_struct 指针。内核第一次遍历task 数组,判断获得的进程号是否可以用 于新的进程。

2.2.3 复制进程信息之前,先将一些数据压栈

进程1 在进程槽中的位置和新进程号确定后,即将创建的新进程1就等于有了身份了。

接下来,系统 要将进程0管理结构拷贝给进程1 的管理结构,这就是进程创建最关键的一步。

2.2.4 初步设置进程1管理结构

  1. 把进程0的管理信息task_struct 全部拷贝到 p指向的进程1的管理信息task_struct 中,这意味着,此时进程1 已具有了进程0 的绝大部分能力。
  2. 然后将进程1 的状态设置为不可中断状态等待状态
    此时,只有内核代码中明确表示将该进程设置为就绪状态,它才能被唤醒。除此之外,没有任何办法将其唤醒,因为进程1 尚未创建完毕,它的管理结构还需要进一步调整,这就导致进程1 还不能马上参与多进程的轮询。为此,这里将它设置为“不可中断等待状态” 就是要绝对避免进程1 受到任何中断类信号的打扰,以免出现混乱。

2.2.5 进程0创建 进程1 的过程中发生时钟中断

2.2.6 从时钟中断返回

2.2.7 调整进程1管理结构

进程1的数据是从进程0 拷贝过来的,但并不是所有的数据信息都 全部适用于进程1, 因此还需要针对具体的情况进行调整。

  1. 从last_pid 得到程1 ,设置 p->pid = last_pid
  2. 进程0 就是 进程1 的父进程,所以就将进程0 的进程号“ 0 ” 作为进程1 的父进程号
  3. 进程0的优先级是15,所以,用优先级的数值来初始化进程1 的时间片,则进程1的时间片也被初始化为15.
  4. 对进程1 的任务状态描述符表 TSS 中的各个成员进行初始化。

此时,进程0 为就绪态,进程 1 为不可中断状态。

2.2.8 设置进程1 的线性地址空间及物理页面

2.2.9 操作系统如何区分进程0 和进程1

2.2.10 进程0 准备切换到进程1

此时,进程0 就开始准备切换到进程1 了,在Linux 0.11 的进程调机制中,有两种情况可以产生切换:

  1. 由于进程运行的时间到了,于是进行切换。一旦当前进程的时间片削减为0 了,说是地这个 进程此次执行的时间用完了。
  2. 由于逻辑执行需要被打断,于是进程切换。

2.2.11 系统切换到进程1执行

接下来进入schedule 函数,开始分析现在有没有必要进行进程切换,如果有必要,再进行具本的切换操作。

  1. 首先依据进程槽task[64] 这个结构体,第一次遍历所有的结果。
// kernel/shced.cvoid shcedule(void){
... for( p=&LAST_TASK; p > &FIRST_TASK ; --P ) if(*p){
if((*p)->alarm && (*p)->alarm < jiffies){
// 针对报警定时值进行处理 (*p)->signal |= (1 << (SIGALRM - 1)); (*P)->alarm = 0; } if( ( (*p)->signal & !(_BLOCKABLE & (*p)->blocked) ) && (*p)->state == TASK_INTERRUPTBLE ) (*p)->state = TASK_RUNNING; } ...}

只要地址指针不为空,就要针对它们的“报警定时值alarm” 及 “信号位图signal” 进行处理,当时还没有具体效果产生,尤其是进程0 此时并没有收到任何信号,所以它的状态仍然是“ 可中断等待状态” , 不可能变为就绪态。

  1. 第二次遍历
// kernel/shced.cvoid shcedule(void){
... for( p=&LAST_TASK; p > &FIRST_TASK ; --P ) if(*p){
if((*p)->alarm && (*p)->alarm < jiffies){
// 针对报警定时值进行处理 (*p)->signal |= (1 << (SIGALRM - 1)); (*P)->alarm = 0; } if( ( (*p)->signal & !(_BLOCKABLE & (*p)->blocked) ) && (*p)->state == TASK_INTERRUPTBLE ) (*p)->state = TASK_RUNNING; } // 第二次遍历 while(1){
c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; while (--i){
if( !*--p ) continue; if( (*p)->state == TASK_RUNNING && (*p)->counter > c ) c = (*p)->counter, next = i; } if(c) break; for( p = &LAST_TASK ; p > &FIRST_TASK ; --p) if( *p ) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } switch_to(next); ...}

2.3 加载根文件系统

  • 第一部分: 进程1 通过对一此与硬盘管理相关的数据结构的设置,为其与硬盘以文件形式进行数据交互初步奠定基础。

  • 第二部分: 进程 1 用虚拟盘替代软盘,使之成为根设备。

  • 第三部分: 进程 1 以虚拟盘中提供的数据为依据,加载根文件系统,从虚拟盘中读取根文件系统的超级块,并将超级块及基附属信息国中载到时 指定数据结构中。

2.3.1 进程1 开始以数据块的形式操作硬盘

进入 getblk 函数,申请的步骤是这样的:

系统要先在缓冲区里面,通过哈希表结构找一下,看看此时有没有哪个缓冲块中所对应的设备号和块号,与当前想要找的那个逻辑块号和设备号相匹配,这是因为,缓冲块将来要大量频繁的被用于主机与外设的数据交互,而存储在缓冲块的数据不会立即消失,
这样要读妈的缓冲块有可能已经在本次读取之前就 存在于缓冲区中了,那么本次就没必要再重新从外设上读取一遍了,直接使用就可以了。

进程1 继续执行,进入get_hash_table函数后,如果能找到指定缓冲块,直接用就 可以了。但由于现在是第一次缓冲区,所以不可能存在匹配的经缓冲块,因此从get_hash_table 函数退出后,只能在空闲链表中新申请一个合适的缓冲块了。

2.3.2 将找到的缓冲块与请求项挂接

2.4 创建进程2

这里将以进程 1 为母本创建进程 2 ,进程 1 已经在进程 0 的基础上拓展了与外设的交互能力,这种能力将在进程 1 创建 进程 2的过程中全部遗传给进程 2。

  1. 进程 1 准备创建进程 2
  2. 复制进程 2 管理结构并进行调整
  3. 设置进程 2的页目录项并复制进程 2的页表
  4. 调整进程 2管理结构中与文件有关的内容
  5. 进程1 执行过程中发生时钟中断
  6. 进程 1 从时钟中断返回,准备切换到进程2

2.5 进程 1 等 待进程 2 退出

  1. 进程 1 查找它自已的子进程
  2. 对进程 2 的状态进行处理
  3. 切换到进程 2执行
  4. shell 程序的加载
    第一阶段是为shell 的加载做准备,主要通过main.c 中的close(),open(),和 execve(), 来实现。
    第二阶段是为根据实际需要,加载shell 程序,主要通过page.s中的call_do_no_page来实现。
    (1)进程 2 为shell 程序的执行做外围准备,如 配置文件,环境变量,参数等。
    (2)进程 2 对shell 程序所在文件进行全方位检测。
    (3)针对shell 程序,对当前进程,即 进程2管理信息进行调整
    (4)进程2 对shell 程序将来要执行的第一条指令的地址值进行调整 。
// init/main.c。。。static char * argv_rc[] = {
"/bin/sh" ,NULL};static char * envp_rc[] = {
"HOME=/", NULL , NULL};。。。

2.6 检测协处理器

2.7 调整shell 程序所在的线性空间地址

2.8 为shell 程序准备参数和环境变量

2.9 继续调整进程2管理结构

2.10 调整EIP ,使其指向 shell 程序入口地址

2.11 shell 程序执行引发缺页中断

2.12 缺页中断中shell 程序加载前的检测

2.13 为即将载入的内容申请页面

2.14 将shell 程序载入新获得的页面

2.15 将shell 程序载入新获得的页面

2.16 根据shell 程序的情况,调整页面的内容

2.17 将线性地址空问与程序 所在的物理页面对应

2.18 系统实现怠速

本文学自《Linux内核设计的艺术》

转载地址:https://ciellee.blog.csdn.net/article/details/104674278 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【Linux内核】---- 03 安装文件系统
下一篇:【Linux内核】---- 01 开机上电初始化过程

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月13日 23时02分04秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

掌握代码背后的这种语言,让你一招通吃天下! 2019-04-29
最终榜单!2019年人工智能的15个热门趋势 2019-04-29
揭秘卷积神经网络热力图:类激活映射 2019-04-29
骂谷歌,怼百度,批腾讯,吴军为何DISS互联网公司没得怕的? 2019-04-29
【Postgresql 基础】查询昨天、本周、本月、上月、本年统计数据 2019-04-29
【Python】Request库的接口测试实例 2019-04-29
【Python】ModuleNotFoundError: No module named ‘fake_useragent‘ 2019-04-29
【Python基础】Python处理Excel文件,进行筛选数据、排序等操作及保存新的Excel文件 2019-04-29
【jmeter二次开发】IDEA导入JMeter源码,进行二次开发自定义函数 2019-04-29
【项目部署】Docker+Jenkins部署Python接口自动化项目 2019-04-29
【Docker】docker run xxx,E Time Elapsed: 0:00:00.000180 2019-04-29
【Docker】docker常用命令 2019-04-29
【问题】ERROR: Command errored out with exit status 1:安装pyinstaller失败原因及解决办法 2019-04-29
【问题】Python打包exe报错:TypeError: an integer is required (got type bytes) 2019-04-29
【问题】jenkins ‘“node“‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。 2019-04-29
【Centos 7】安装Pyhton3.8 2019-04-29
【Python调试】Python中调试模块 2019-04-29
【PostMan使用】PostMan的简单使用教程 2019-04-29
【PyCharm设置】PyCharm取消,双击shift弹出来的搜索框 2019-04-29
【Python爬虫】js反反爬策略之有道翻译,附代码 2019-04-29