重读 xv6(VII)
$ echo hello | cat > hello.txt
九 #
我们已经讨论了 xv6 所有的模块设计,是时候出一个总集篇了
以下是我们要回答的终极问题:
当用户输入并运行
echo hello | cat > hello.txt时,xv6 内部发生了什么?
敲下键盘之前 #
源码文件:sh.c、util.c
……shell 正在运行,打印出
$,调用util/gets等待用户输入命令
char*
gets(char *buf, int max)
{
int i, cc;
char c;
for(i=0; i+1 < max; ){
cc = read(0, &c, 1);
if(cc < 1)
break;
buf[i++] = c;
if(c == '\n' || c == '\r')
break;
}
buf[i] = '\0';
return buf;
}
Trap 到内核,调用 sys_read
嵌套调用 fileread,发现文件描述符 0 指向 device,调用驱动程序 consoleread
等待用户输入……
人机交互 #
用户按下键盘,输入字符
e
此时,uart 检测到串行接口的寄存器发生改变,认为用户发起了一次写入操作,向 PLIC 提出一次设备中断
硬件检测到中断,
sys_read线程通过kernelvec陷入到驱动程序
kerneltrap 检测到 %scause 类型为 uart,调用对应的驱动函数 uartintr
嵌套调用 consoleintr ,从寄存器得知,输入字符为 e
console 更新其缓冲区 buffer 并调用 uartputc 将字符 e 回显到屏幕
从 uart 驱动返回到 consoleread 线程,从 buffer 读入字符 e
用户继续输入剩余的
cho hello | cat > hello.txt
此时 consoleread 一直处于循环,等待 \n 结束本行输入
最后用户敲击
Enter键,consoleread检测到\n,结束输入
consoleread 结束后,从中断返回到 sys_read 线程
sys_read 结束读入,返回到 gets
gets 结束输入,getcmd 结束,shell 完成 command 字符串输入
解析命令 #
shell读取到字符串echo hello | cat > hello.txt
shell 按 token 解析命令:
- 读入
echo hello - 检查到
|,认为有管道,分离出cat > hello.txt - 读入
cat - 检查到
>,认为有重定向,输出设置为hello.txt
随后 shell 调用 runcmd 执行命令:
- 先检测到
PIPE,建立管道 fork出两个进程,重定向 IO 至管道,递归执行两个cmd- 检测到
cat > hello.txt是REDIR命令 - 调用
sys_open打开hello.txt,重定向输出流 - 分别执行
echo与cat
以上是
shell的运行路径,下面我们回到runcmd执行前
管道 #
管道的实现与 console 类似,采用循环 buffer、双指针读写阻塞实现
调用 sys_pipe 时,会用 kalloc 动态申请空间
使用管道时,通过 dup/close 做文件描述符上的变换,将管道的一端指向 0/1 来重定向 IO
shell创建pipe,将echo、cat连接到管道上
文件 #
shell调用sys_open,创建hello.txt文件
open 扫描当前 path 下的目录,发现没有 hello.txt 文件,于是调用 create 创建
而 create 移动到对应的 path,调用 ialloc 获取 inode 资源,再将索引 link 加入到目录文件下,完成空文件创建
完成
command环境的创建,到这里我们才开始正式运行命令
稍等一下 #
shell成功fork出两个进程,等待执行exec
我们忽略了一些东西:
Trap到系统调用的过程fork管理的进程资源schedule调度到新进程
让我们把时间稍稍前调,来到
fork执行前……
sys_fork #
现在,
fork刚刚要执行,这是一个系统调用
将系统调用号放到指定的寄存器,执行 ECALL 汇编指令,发出中断
硬件检测到中断,跳转到
TRAPPOLINE中的uservec
uservec 从 trapframe 中更换上下文,调用 sys_fork,复制进程资源
Fork首先向proctable请求UNUSED进程资源,初始化页表、trapframe随后,按pagetable逐个复制内存到新进程,其他变量也一并复制
fork 结束,从 userret 返回用户空间
调度 #
现在我们有三个 shell 进程了,准备好调度进程
时钟发出中断信号,硬件中断,调用
yield让出 CPU
切换到 scheduler 线程,调用swtch,瞬间替换 context
此时切换到新进程的 forkret,调用 userret 回到用户态,顺利完成进程调度
exec #
调用 exec 读入用户代码
echo与cat,创建user stack,重定向程序流
执行完成后,我们现在拥有三个进程 shell、echo、cat
exec 并不改变 proc 已经打开的描述符,因此重定向 IO 不受影响
运行命令 #
echo与cat调用read/write进行服务
不过,我们已经将 0, 1 进行重定向,因此数据都通过文件或管道流动
hello #
运行结束后,我们将输出结果
hello写入hello.txt
这里就直接讨论 write 的流程:
- 找到
hello.txt的inode文件 - 查找
inode的映射表,找到要写入的物理地址 - 调用
bwrite,获取磁盘的buffer block映射 - 将
hello写入log层 - 事务结束后,
commit将log真正写入到磁盘
写入结束,
shell准备读取下一条命令
虚拟内存 #
有关虚拟内存的流程比较碎,没有提到,主要是以下部分:
kalloc动态分配空间- 取地址时,硬件总是通过页表翻译出真实地址
- 读写时,需要用到
copyin / copyout加上页表,在不同线程之间进行内存通信
后言 #
最后这个问题模仿了计网自顶向下的某一章,但我的笔力驾驭不了整个运行过程,花了太多篇幅在 shell 上,安排比较随意
如果能真正在头脑中运行这一串程序流,不需要弄清每个细节,只需要拨开层层抽象的洋葱,我想对 xv6 架构就认识的足够了,这也是我做源码回顾的动机