《Linux内核设计的艺术》——3.进程1

0. 前言

现在已经有了处于特权3的进程0,将使用fork出进程1,之后的进程也使用fork。

void main()
{
   sti();
   move_to_user_mode(); // 切换到特权3
   if (!fork()) {
      init();       // 进程1进行init
   }
   for(;;) pause(); // 进程0循环进入可中断阻塞态
}

1. fork


fork函数使用汇编实现 int 80 中断, __NR_fork 是 sys_fork 在 sys_call_table 的偏移,
所以调用 sys_fork.
注意,中断使CPU硬件自动将 进程上下文(寄存器)压入 进程0的内核栈,这些压栈的数据将在 copy_process 函数中用于初始化进程的 TSS.
注意,EIP指向 指令 "int $0x80" 的下一行,即 if (__res >= 0)。这是中断返回后第一条执行的指令。进程1也是从这里开始执行。

1.1 sys_fork


注意 sys_fork,在调用 find_empty_process后,将其返回值(空闲的task[i]的编号)%eax 压栈,另外还将 TSS 压栈。所以 父进程的 TSS ,和 空闲的 task[i] 的编号 作为参数,调用 copy_process

1.2 find_empty_process


找到空闲的 task[i] ,返回编号

1.3 copy_process

工作内容如下:

  1. 为进程1创建 task_struct ,将进程0的task_struct 内容复制给进程1
    2)为进程1的task_struct,tss做个性设置
    3)为进程1创建第一个页表,将进程0的页表项内容赋给进程1的页表项
    4)进程1共享进程0的文件会话
    5)设置进程1的GDT项
    6)将进程1设置为就绪态,使其可以参与进程间的调度。

1.3.1 创建task_struct


copy_process有很多参数,都是 sys_fork 压栈的,主要为 新进程的task[i]的i,和父进程的TSS.
get_free_page获得空闲页面。


对于分页的疑惑可以参考进程0创建过程中对分页机制的创建。

copy_process后部分代码

用 *p = *current; 让子进程继承父进程的属性,然后再进程独特设置

注意:
task_struct 和 内核栈的关系

copy_process中对子进程 TSS的设置


注意

p->tss.eip = eip;
p->tss.eax = 0;

这样中断返回后,子进程执行 fork() 中 if(__res >= 0),且 __res 为0

1.3.2 设置进程1的分页管理

copy_process中对分页的设置

copy_mem

先获得 父进程的LDT,然后根据子进程的进程号,找到子进程的 LDT(在GDT中),
然后设置子进程的LDT, set_base(p->ldt[1], new_code_base); set_base(p->ldt[2], new_data_base);
最后调用 copy_page_tables 将父进程的页表 复制给子进程(用 父子进程的 LDT 基地址做参数)。

copy_page_tables

copy_page_tables会申请一个空闲的页面(基于内核的页表),作为子进程的页表空间,将父进程的页表复制给子进程,最后,用重置CR3的方法刷新页变换高速缓存。
注意:只复制了一个页表,因为一个页表有160个页表项,每个页表项对应一个页面,一个页面4KB,所以一个页表控制640KB,远大于进程0的 数据和代码占用空间。
实际上 copy_page_tables 会复制父进程全部的页表。

最终,进程0和进程1的页表,共同控制 640KB的空间。

1.3.3 进程1共享进程0的文件

1.3.4 设置进程1在GDT中的表项

这样内核就能通过 GDT 控制进程1的 LDT(程序和数据),TSS(上下文)

1.3.5 将进程1置为就绪态

1.4 fork返回

2. 内核第一次调度

Linux0.11一下情况发生调度
1)进程运行时间结束
进程在创建时,都被赋予了有限的时间片,以保证所有进程每次都只执行有限的时间。一旦进程的时间片被削减为0,就说明这个进程此次执行的时间用完了,立即切换到其他进程去执行,实现多进程轮流执行。
2)进程阻塞

2.1 pause

此时运行的是进程0,所以调用pause,
最终调用 sys_pause

进程状态变为 可中断态,并调用 schedule,

2.2 schedule

schedule先遍历 task[64],检查进程是否收到信号(alarm , signal 字段),若收到则 将 状态从 可中断态 变为 就绪态。
再次遍历 task[64],根据进程状态和时间片,找出就绪态,且counter最大的进程,执行 switch_to(next),调度进程。


switch_to

此时会发生任务切换,将进程1的TSS数据和LDT代码段,数据段描述符数据 复制给 CPU的各个寄存器,实现从特权0的内核代码切换到特权3的进程1代码执行。

注意:
进程0通过 pause调用 int80 中断,进入内核态,并调用 switch_to,切换到进程1,但进程0没有返回。

3. 进程1执行

根据 copy_process时,当时设置进程1的 tss.eip,指向 if(__res>=0),
所以进程1从 fork() 的 if(__res>=0) 开始执行。
由于 copy_process时,设置 p->tss.eax = 0,所以 __res为0


所以进程1 调用 init()

3.1 init

init先调用 setup

3.2 setup

setup也通过int80中断调用 sys_setup,
注意,进程0的进程还没有返回。

sys_setup为安装硬件文件系统做准备
1)根据机器系统数据设置硬盘参数
2)读取硬盘引导块
3)从引导块中获取信息

根据机器系统数据drive_info(柱面,磁头数,扇区数)设置内核的hd_info

读取引导块到缓冲区
引导块中有硬盘的 分区表,根据分区表可以引导处其他信息。
一个硬盘只有一个引导块,一个引导块有两个扇区,但真正有用的只有一个扇区。
使用 bread读取那个扇区

3.2.1 bread


先使用getblk获得对应的缓冲块,再读取。

3.2.1.1 getblk

getblk用 设备号,和块号 做哈希参数,获得缓冲块


由于还没有读取过,所以没有和 dev, blk 对应的缓冲块,所以 从 空闲列表中分配

一定能获得空闲的缓冲块

将获得缓存块,加入哈希表

对缓存块进行设置

remove_from_queues(bh); 将缓冲块从空闲表中删除

3.2.1.2 ll_rw_block

break函数 通过 getblk获得缓存块后,调用ll_rw_block,将缓冲块和请求项挂接

图中黑色部分为缓存块数组,在内核前部分已经完成格式化。

make_request会对缓存块进行加锁,
然后申请一个空的请求项,和缓冲块挂接。

make_request




lock_buffer

add_request
add_request调用 dev->request_fn, request_fn 是前期绑定的回调,这里是 do_hd_request

do_hd_request
先通过对当前请求项数据成员的分析,解析出需要操作的磁头、扇区、柱面、操作多少个扇区……之后,建立硬盘读盘必要的参数,将磁头移动到0柱面,如图
3-22中第二步所示;之后,针对命令的性质(读/写)给硬盘发送操作命令。现在是读操作(读硬盘的引导块),所以接下来要调用hd_out()函数来下
达最后的硬盘操作指令。注意看最后两个实参,WIN_READ表示接下来要进行读操作,read_intr()是读盘操作对应的中断服务程序,所以要提取
它的函数地址,准备挂接,这一动作反映在图3-22中的第三步。请注意,这是通过hd_out()函数实现的,读盘请求就挂接read_intr();如
果是写盘,那就不是read_intr(),而是write_intr()了。

.... 中间为分析磁盘参数

hd_out


hd_out:
do_hd = intr_addr; // 将磁盘中断处理方法和中断服务程序绑定

在硬盘中断时

xchgl_do_hd, %edx 会调用绑定的回调函数

3.2.2 ll_rw_block返回

hd_out最后下发硬盘命令,
硬盘开始将引导块中的数据不断读入它的缓存中,同时,程序也返回了,将会沿着前面调用的反方向,即hd_out()函数、do_hd_request()
函数、add_request()函数、make_request()函数、ll_rw_block()函数,一直返回bread()函数中。

3.2.3 wait_on_buffer

由于硬盘数据没有读完,所以调用wait_on_buffer,挂起等待

sleep_on 将进程1设置为不可中断态,然后调用 schedule

进入schedule函数后,切换到进程0去执行,
此时进程0的EIP指向 cmpl%%ecx, _last_task_used_match\n\t

最后进程0会进入 for(;😉 pause(); 直到硬盘中断发生

3.2.4 硬盘中断



首先保存进程上下文,
然后 call*%edx 调用hd_out绑定的函数。
这里是 read_intr

3.2.5 read_intr


read_intr()函数会将已经读到硬盘缓存中的数据(硬盘上有个高速缓存,不是指主板的内存)复制到刚才被锁定的那个缓冲块中(这里是内存)(注意:锁定是阻止进程方面的操作,而不是阻止外设方面的操
作),这时1个扇区256字(512字节)的数据读入前面申请到的缓冲块
由于引导块的大小是1024字节,请求项要求的就是1024字节,所以现在只读了一半,所以 会再次把 read_intr绑定到硬盘中断服务程序上。

又过了一段时间后,硬盘剩下的那一半数据也读完了,硬盘产生中断,读盘中断服务程序再次响应这个中断,进入read_intr()函数后,仍然会判断请求项对应的缓冲块的数据是否读完了,对应代码如下:

end_request工作如下


end_request会调用 unlock_buffer,解锁缓冲块,并将等待的进程设置为就绪态,并将请求项设为空闲。

中断处理程序返回后,进程0继续 sys_pause, schedule, switch_to,
而这时进程1的状态为就绪态,所以切换到进程1.

3.2.6 wait_on_buffer返回


wait_on_buffer返回,由于 bh->b_update为1,所以bread返回

3.2.7 检查引导块信息

回到 sys_setup函数


首先对引导块信息进行检查,然后用引导信息设置 hd[],最后用 brelse 释放缓冲块。

3.2.8 进程1格式化虚拟盘

进程0初始化时,为虚拟盘分配了内存空间,并初始化为0,现在要对其进行格式化。
格式化所需要的信息在软盘上,
其中 第一个扇区时bootsect,后4个扇区是 setup,后240个扇区是head和system模块,
所以 前245个扇区都是 kernel,而 rootfs在256扇区开始。

进程1使用 rd_load 进行格式化虚拟盘
rd_load调用 breada 读取257,256,258,引导块在 256,超级块在257.
读入的信息在缓冲块。
之后分析信息,判断是否是minix文件系统,根文件系统的数据大小 等,分析完毕后释放缓存块


接下来调用breada函数,把与文件系统相关内容从软盘上拷贝到虚拟盘,然后释放缓冲块,完成格式化。
拷贝结束后,将虚拟盘设置为根设备

3.2.9 进程1在根设备上加载根文件系统

文件系统是用来管理文件的。文件系统用i节点来管理文件,一个i节点管理一个文件,i节点和文件一一对应。文件的路径在操作系统中由目录文件中的目录项管
理,一个目录项对应一级路径,目录文件也是文件,也由i节点管理。一个文件挂在一个目录文件的目录项上,这个目录文件根据实际路径的不同,又可能挂在另一
个目录文件的目录项上。一个目录文件有多个目录项,可以形成不同的路径。效果如图3-35所示。

所有的文件(包括目录文件)的i节点最终挂接成一个树形结构,树根i节点就叫这个文件系统的根i节点。一个逻辑设备(一个物理设备可以分成多个逻辑设备,
比如物理硬盘可以分成多个逻辑硬盘)只有一个文件系统,一个文件系统只能包含一个这样的树形结构,也就是说,一个逻辑设备只能有一个根i节点。

加载文件系统最重要的标志,就是把一个逻辑设备上的文件系统的根i节点,关联到另一个文件系统的i节点上。具体是哪一个i节点,由操作系统的使用者通过mount命令决定。

另外,一个文件系统必须挂接在另一个文件系统上,按照这个设计,一定存在一个只被其他文件系统挂接的文件系统,这个文件系统就叫根文件系统,根文件系统所在的设备就叫根设备。

别的文件系统可以挂在根文件系统上,根文件系统挂在哪呢?

挂在super_block[8]上。

Linux 

0.11操作系统中只有一个super_block[8],每个数组元素是一个超级块,一个超级块管理一个逻辑设备,也就是说操作系统最多只能管理8个逻
辑设备,其中只有一个根设备。加载根文件系统最重要的标志就是把根文件系统的根i节点挂在super_block[8]中根设备对应的超级块上。

可以说,加载根文件系统有三个主要步骤:

1)复制根设备的超级块到super_block[8]中,将根设备中的根i节点挂在super_block[8]中对应根设备的超级块上。

2)将驻留缓冲区中16个缓冲块的根设备逻辑块位图、i节点位图分别挂接在super_block[8]中根设备超级块的s_zmap[8]、s_imap[8]上。

3)将当前进程的pwd、root指针指向根设备的根i节点。

进程1通过调用mount_root()函数实现在根设备虚拟盘上加载根文件系统。执行代码如下:

进入mount_root()函数后,初始化内存中的超级块super_block[8],将每一项所对应的设备号加锁标志和等待它解锁的进程全部设置为
0。系统只要想和任何一个设备以文件的形式进行数据交互,就要将这个设备的超级块存储在super_block[8]中,这样可以通过
super_block[8]获取这个设备中文件系统的最基本信息,根设备中的超级块也不例外,如图3-38所示。



接下来调用 read_super,从虚拟盘中读取根设备的超级块,复制到 super_block[8] 中


read_super找到空闲的 super_block,并锁定。

之后通过 bread 从虚拟盘读取 super_block,
超级块复制到缓存块后,将缓冲块中的超级块复制到 super_block[0]。
从现在起,虚拟盘这个根设备就用 super_block[0]来管理,之后调用 brelse释放缓冲块。

初始化super_block[8]中的虚拟盘超级块中的i节点位图s_imap、逻辑块位图s_zmap,并把虚拟盘上i节点位图、逻辑块位图所占用的
所有逻辑块读到缓冲区,将这些缓冲块分别挂接到s_imap[8]和s_zmap[8]上。由于对它们的操作会比较频繁,所以这些占用的缓冲块并不被释
放,它们将常驻在缓冲区内。

如图3-41所示,超级块通过指针与s_imap和s_zmap实现挂接。


回到mount_root()函数中,调用iget()函数,从虚拟盘上读取根i节点。根i节点的意义在于,通过它可以到文件系统中任何指定的i节点,也就是能找到任何指定的文件。

进入iget()函数后,操作系统从i节点表inode_table[32]中申请一个空闲的i节点位置(inode_table[32]是操作系统用来
控制同时打开不同文件的最大数)。此时应该是首个i节点。对这个i节点进行初始化设置,其中包括该i节点对应的设备号、该i节点的节点号……图3-42中
给出了根目录i节点在内核i节点表中的位置。




在read_inode()函数中,先给inode_table[32]中的这个i节点加锁。在解锁之前,这个i节点就不会被别的程序占用。之后,通过该
i节点所在的超级块,间接地计算出i节点所在的逻辑块号,并将i节点所在的逻辑块整体读出,从中提取这个i节点的信息,载入刚才加锁的i节点位置上,如图
3-43所示,注意inode_table[32]中的变化。最后,释放缓冲块并将锁定的i节点解锁。

回到iget()函数,将inode指针返回给mount_root()函数,并赋给mi指针。

下面是加载根文件系统的标志性动作:

将inode_table[32]中代表虚拟盘根i节点的项挂接到super_block[8]中代表根设备虚拟盘的项中的s_isup、s_imount指针上。这样,操作系统在根设备上可以通过这里建立的关系,一步步地把文件找到。


3. 将根文件系统与进程1关联

对进程1的tast_struct中与文件系统i节点有关的字段进行设置,将根i节点与当前进程(现在就是进程1)关联起来,如图3-44所示。


最后根据根据文件系统的超级块 中的 逻辑块位图,计算出 虚拟盘上 数据块的占用和空闲情况,并将信息记录到 驻留在缓冲区中 "装载了逻辑块位图信息的缓冲块中"(前面通过 bread 读取位图使后,相关缓存块并没有释放,长期驻留)。

3.2.10 sys_setup执行完毕

由于 sys_setup 是软中断导致调用的,所以返回 system_call 执行,之后会执行 ret_from_sys_call。
这时当前进程是进程1,所以调用 do_signal函数,对当前进程的信号位图进行检查。

当前进程没有收到信号,所以do_signal没有意义,
至此 setup返回,

至此,进程0创建进程1,进程1为安装硬盘文件系统做准备、“格式化”虚拟盘并用虚拟盘取代软盘为根设备、在虚拟盘上加载根文件系统的内容讲解完毕。

posted on 2022-08-17 15:24  开心种树  阅读(164)  评论(0编辑  收藏  举报