《Linux内核设计的艺术》——第4章

现在已经有进程0,进程1,并挂载根文件系统,接下来创建进程2,并加载shell。

1. 打开标准输入,标准输出,标准错误

加载完根文件系统后,进程1调用open打开标准输入

open产生软件中断 int0x80,根据在IDT中的偏移,进入sys_open函数。
sys_open 分配 文件描述符,分配 struct file,并将两者关联,然后调用 open_namei 获得文件i节点。

这一目标是通过不断分析路径名来实现的。分析工作的第一阶段是调用dir_namei()函数,获取枝梢i节点,即/dev/tty0路径中dev目录文
件的i节点;第二阶段是调用find_entry()函数,通过此i节点,找到dev目录文件中tty0这一目录项,再通过该目录项找到tty0文件的i
节点。

dir_namei()函数中将首先调用get_dir()函数来获取枝梢i节点,之后再通过解析路径名,获取tty0目录项的地址和文件名长度信息。调用get_dir()函数的具体执行代码如下:

值得注意的是,get_fs_byte()函数是解析路径的核心函数,可以从路径中逐一提取字符串。该函数在后面具体的路径解析工作中还会用到,它的内部处理过程如下:

get_dir()函数首先确定路径的绝对起点,即分析"/dev/tty0"这个路径名的第一个字符是不是'/'。如果是'/',就确定这是绝对路径
名,因此将从根i节点开始查找文件。这个根i节点已在第3章3.3.3节中加载根文件系统时载入,它被确定为路径绝对起点,同时,它被引用,其引用计数也
随之增加。这部分执行代码如下:

上面部分完成了对 /dev/tty0 中 '/' 的解析,确定了第一个目录inode。
下面获得 dev 的inode

struct dir_entry 是一个目录项

最后 dev的inode也保存到 内核的inode_table[32],并且进程1的内核栈指向他

获得枝梢inode的流程如下

这样 dir_namei 就返回两个有效数据,枝梢目录inode和打开文件的文件名

回到open_namei,获得 tty0的inode



回到 sys_open,分析 tty0文件inode的属性,得知他是设备文件,再通过i节点的i_zone[0],确定设备号,并对current->tty和 tty_table进行设置。

sys_open()最后要针对file_table[64]中与进程1的filp[20]对应的表项file_table[0]进行设置。这样,系统通
过file_table[64],建立了进程1与tty0文件(标准输入设备文件)i节点的对应关系。执行代码如下:

返回open,完成标准输入的打开,然后使用dup打开标准输出和标准错误



2. 创建进程2,并切换到进程2


fork -> sys_fork ,
sys_fork 先调用 find_empty_process 为进程 2找到空闲的 task,再调用 copy_process函数

进入copy_process()函数后,会为进程2的task_struct以及内核栈申请页面,并复制task_struct,随后对进程2的
task_struct进行各种个性化设置,包括各个寄存器的设置、内存页面的管理设置、共享文件的设置、GDT表项的设置等。


fork返回后,进程1执行 wait


此时进程2为就绪态,导致 flag设置1,并退出循环

然后进程1设置状态为 可中断态,并schedule

进程2从fork返回,执行

先执行close 标准输入

然后打开 /etc/rc,由于close释放了文件描述0,所以这次open返回0,所以 /etc/rc作为输入
之后调用 execve("/bin/sh", argv_rc, envp_rc)
注意,环境遍历和参数都是在system模块写好的

execve最终调用 do_execve


然后通过inode提供的设备号和块号,将文件头载入缓存区,获得信息。


获得文件头后,将其复制到内核栈,并对其进行检查

设置参数和环境变量的管理指针表page,并统计参数和环境变量个数,最终将它们复制并映射到进程2的栈空间中。


进程2有了自己对应的程序shell,因此要对自身task_struct进行调整以适应此变化。比如,原来与其父进程(进程1)共享的文件、内存页面,现在要解除关系,要根据shell程序自身情况,量身定做LDT,并设置代码段、数据段、栈段等控制变量。

对sys_execve软中断压栈的值进行设置,用shell程序的起始地址值设置EIP,用进程2新的栈顶地址值设置ESP。这样,软中断iret返回后,进程2将从shell程序开始执行。代码如下:

do_execve函数返回,sys_execve中断返回,执行shell程序(因为EIP,ESP已经改了)。

3. 执行shell

shell程序开始执行后,其线性地址空间对应的程序内容并未加载,也就不存在相应的页面,因此就会产生一个“页异常”中断。此中断会进一步调用“缺页中断”处理程序来分配该页面,并加载一页shell程序。执行代码如下:

do_no_page()函数开始执行后,先确定缺页的原因。假如是由于需要加载程序才缺页,会尝试与其他进程共享shell(显然此前没有进程加载过
shell,无法共享),于是申请一个新的页面,并调用bread_page()函数,从虚拟盘上读取4块(4
KB、一页)shell程序内容,载入内存页面。具体执行代码如下:

载入一页的Shell程序后,内核会将该页内容映射到shell进程的线性地址空间内,建立页目录表→页表→页面的三级映射管理关系。具体执行代码如下:


shell程序开始执行后,要读取标准输入设备文件上的信息,即task_struct中filp[20]第一项所对应文件的信息。
进程2,即shell进程刚开始执行,就用rc文件替换了标准输入设备文件tty0,因此,shell程序执行后读取的是rc文件上的信息。

根据/etc/update这条命令,shell先创建一个新进程。这个新进程的进程号是3(shell进程的进程号是2,依次累加,所以它的进程号就是
3)。它在task[64]中的“项号”也是3。我们在后面称之为“update进程”。创建完毕后,加载update程序,并最终将执行权转交给
update进程,由它去执行。这一创建、加载、切换的过程,

update进程有一项很重要的任务:将缓冲区中的数据同步到外设(软盘、硬盘等)上。由于主机与外设的数据交换速度远低于主机内部的数据处理速度,因
此,当内核需要往外设上写数据的时候,为了提高系统的整体执行效率,并不把数据直接写入外设上,而是先写入缓冲区,之后,根据实际情况,再将数据从缓冲区
同步到外设。

每隔一段时间,update进程就会被唤醒,把数据往外设上同步一次,之后这个进程会被挂起,即被设置为可中断等待状态,等待着下一次被唤醒后继续执行,如此周而复始。

update进程执行后,并没有同步任务,于是该进程被挂起,系统进行进程调度,最终切换到shell进程继续执行。

hell进程处理了rc文件中的第一条命令,创建了update进程。现在处理第二条命令,即echo"/dev/hd1/"> /etc/mtab,将"/dev/hd1/"这一字符串写入虚拟盘中/etc/mtab文件,执行完毕后,shell程序会继续循环调用read()函
数读取rc文件上的内容。read()函数对应的系统调用函数是sys_read。代码如下:

由于 read的是普通文件,读到文件结束,导致shell退出,执行exit



sys_exit除了释放资源,还调用 tell_father 和 schedule。
tell_father会给进程1发SIGCHLD信号

可见发送信号,就是设置接受信号的进程的 task->signal
然后调用schedule

进程1执行,而进程1返回到sys_waitpid schedule下一行,继续执行

于是进程1对僵死进程进行处理,并返回进程2的进程号

进程1再次回到init

4. 再次运行shell


这次运行shell前,设置其标准输入为 /dev/tty0.

这导致shell不会退出

进入 rw_char后,shell进程被设置为可中断状态。这样所有的进程都处于可中断状态,再次切换到进程0,系统进入怠速。

怠速以后,操作系统用户将通过shell进程提供的平台与计算机进行交互。shell进程处理用户指令的工作原理如下:用户通过键盘输入信息,存储在指定
的字符缓冲队列上。该缓冲队列上的内容,就是tty0文件的内容。shell进程会不断读取缓冲队列上的数据信息。如果用户没有下达指令,缓冲队列中就不
会有数据,shell进程将会被设置为可中断等待状态,即被挂起。如果用户通过键盘下达指令,将产生键盘中断,中断服务程序会将字符信息存储在缓冲队列
上,并给shell进程发信号,信号将导致shell进程被设置为就绪状态,即被唤醒,唤醒后的shell继续从缓冲队列中读取数据信息并处理,完毕后,
shell进程将再次被挂起,等待下一次键盘中断被唤醒。

posted on 2022-08-18 11:06  开心种树  阅读(138)  评论(0编辑  收藏  举报