NJUOS-11-操作系统上的进程
本文最后更新于:2 年前
进程结束啦,状态机专题也结束了。进入虚拟化的部分了,但是状态机用来理解进程也是有非常重要的意义的昂!!!
OS所有的一切都是状态机管理!
- 本次课的主题:
- 操作系统启动后到底做了什么
- 操作系统如何管理程序(进程)
操作系统启动后到底做了什么?
真正的操作系统做的事情就是加载系统里面的第一个程序,然后把所有控制权都交给这个程序。系统里面就有一个程序在执行,系统里面的进程都是组成一棵树状的结构。实际上树根那个程序,就是操作系统拉起来的。
- can can need?
可以看到,这里操作系统就是逐个去找可能存在的systemd的进程,就是我们说的第一个程序,也就是树的根。找到了就启动,找不到panic了,操作系统启动不了昂!!!这个根就提供了很多系统调用,比如:创建一个新的程序之类的昂!!!
1 |
|
在mac上,其实也能看到结构的嗷,这个根,包括对应的进程所在的位置!
从系统启动到第一个进程
虚拟机(最小linux的启动与测试使用)
没有存储设备,只有包含两个文件的 “initramfs”
1 |
|
加上 vmlinuz (内核镜像) 就可以在 QEMU 里启动了
上面就是一个最小的Linux,可以不断往里面加文件,尝试去跑!!!
1 |
|
通过make指令可以成功编译,并且获得一个initramfs.cpio.gz镜像,挂载了就能跑的那种。
- 直接跑起来:make run
1 |
|
因为我们没有下载qemu嘛,下载了就好昂!brew install qemu
- 再次make run之后,我们其实就用虚拟机启动了一个最小的状态,通过info registers之类的指令,就可以看到系统当前的状态。
这里建议看看原视频昂!
1 |
|
可以直接在linux运行busybox,其中包含了各种各样我们常用的命令行指令,虚拟机中直接执行命令是用不了的昂,要用里面的Busybox!
魔术来咯
上面的虚拟机里面,命令都用不了orz
BusyBox: The Swiss Army Knife of embedded Linux
1 |
|
在上面啥都用不了的虚拟机里面,把上述的指令拷贝进去。。。???可以直接在终端执行linux指令了!!!
本质上就是把busybox链接到/bin/目录下了,生成的链接文件的名字为ps, arch, ash, base64等linux常用命令的名字!
重启整个虚拟机的话:make && make run
- 试试 adb shell (toybox) -> 比busybox更简陋的一个命令行工具。一样的,符号链接,可以使用昂!!!
可以直接在文件系统中添加静态链接的二进制文件
手动放到linux-minimal/initramfs的code目录下,启动:
gcc -c minimal.S && ld minimal.o -> ./a.out
gcc logisim.c -o logisim -static
- 静态链接的话,会有点大 -> 没关系。但是我们发现,我们在code目录下,mac编译好的这些输出的结果。在我们的qemu虚拟机,也就是在最小的linux环境下,一样是可以跑出来的!!!
我们最小的操作系统的dev,只有console昂!
这里最小的linux里面,不也就是一个进程。然后在这个进程上面,运行别的程序吗?对不!!!
注意,init脚本里面有很多链接的过程被注释掉了,本质上就是我们上面手动执行的脚本,把注释掉的链接打开。直接make && make run,就能得到一个有体验的最小的linux系统昂!
- 最小的Linux很好玩!!!
- 足够真实,真的是一个Linux的环境!
- 足够小,啥都没有哈哈哈哈,可以帮我们理解很多东西。
2021 年,CCF 以迅雷不及掩耳盗铃之势发布了 NOILinux 2.0
- Ubuntu 20.04 Desktop (x86-64 only)
- 真就不管那些 32-bit 的老爷机和老爷系统的死活了?
和刚才的 “最小” 系统但本质一样
- 有更多设备 (磁盘、网卡等)
- initramfs 里挂载了磁盘
- 磁盘里安装了最少的编译运行环境 (g++, …) 和一个 Web 服务
switch_root
(pivot_root
系统调用) 完成 “启动”
和上面类似的!!!要真的模拟整个x64,x32根本顶不住啊。。。简单的呢?精简的文件系统,32-bit上跑64-bit,由于IO竞赛,只提供部分的外设(比如挂载磁盘,方便选手提交答案)还有运行环境,比如gcc之类的,不是就!!!很小的一个运行环境吗!!!
总结:
syscall三大类:进程管理,内存管理,文件管理。有这三个调用,其实就能够构造出一个小小世界了!!!有这三个就可以写出一个小小的操作系统了昂!!!
进程
操作系统创建了第一个进程以后,操作系统只有一个进程。后面这些进程,该怎么产生呢?
- 创建进程的API -> fork()
fork()
C 程序 = 状态机
- 初始状态:
main(argc, argv)
- 程序可以直接在处理器上执行
虚拟化:操作系统在物理内存中保存多个状态机
- 通过虚拟内存实现每次 “拿出来一个执行”
- 中断后进入操作系统代码,“换一个执行”
状态机角度之Fork: 把进程这个状态机,复制了一份,复制出来的状态机和原来的状态机,完全一样的,而且相互独立的!!!内存的每个字节都一样,寄存器也都一样。除了返回值不一样(返回值在eax寄存器里面)
被创建出来的fork()返回值为0
- 操作系统的模型就变了!!!从原来的一个程序,变成了多个进程。 -> 模型就变成了并发模型!!!
什么是操作系统?状态机的管理者
什么是虚拟化?操作系统里面可以管理好多个状态机,虽然容纳了很多状态机,但是每一次只能选一个状态机执行,这就是虚拟化!
- 有个有意思的bomb:
小的例子理解fork()
example 1
试着拿出一张纸,写出以下程序的输出结果
- fork-demo.c
- 你心里可能立即想,运行一下不就完了吗?
- 记得用 “状态机” 去理解它
1 |
|
- Result:
1 |
|
涉及到了并发,状态机开始复杂了…
main — (main -> pid1) — (main -> pid1 -> pid2) || (main -> pid1 && main -> pid2) — 再往下更乱呜呜呜!
注意,fork的时候,是fork整个状态机昂!!!比如如果pid1执行了fork(),这个时候的fork(),是把pid1整个拷贝了一份(包括当前执行到的位置之类的)。pid1和main是并发执行的,所以从main()中fork()出来的进程,和pid1()中for()出来的,是不一样的昂!!!
example 2
1 |
|
预测:应该是六个
- Result:
1 |
|
奇怪了,命令行输出是6行,但是统计输出的行数是八行???
计算机系统里没有魔法。机器永远是对的。
- 本质上是printf有问题昂!
上面这里,就算有\n,但是管道到另外一个文件中./a.out | less,”Hello”也没有了…
我们的fork()是无情的拷贝机器,它会忠实地把所有的寄存器和所有的内存都赋值一遍,不管这些变量有什么含义。 -> 所以它会把那些库函数的内部状态,同样也复制一份。
fflush(stdout) -> 会执行一个系统调用,真正把字符输出到文件里面去(库函数,会执行一个底层的系统调用)。
buffer详解
- buffer其实有两种
- line buffer
- full buffer
tty(终端)是line buffer: 如果看到一个\n,就会把所有缓冲区的东西,用系统调用写出来。
pipe/file是full buffer: 写满4096B(一个页面)之后,才会丢给系统调用,把这个页面输出出来。
fflush本质上就是调用系统调用,把缓冲区内的内容打印出来。
tty的分析我们上面做过了,就是6个,对于pipe/file,我们应该怎么分析呢?
- pipe/file中的full buffer的分析:
所以最后线程结束的时候,把所有的Hello\n输出到pipe中去,总共就是8个Hello\n。什么叫“忠实“的复制?忠实就是把printf这种内部库的buffer,也复制了!!!它会把那些库函数的内部状态,全部也同样复制一份。
很有意思!!!
- 能否改变这个行为?
1 |
|
设置为不缓冲!!!所有的printf都会被直接翻译为系统调用!!!
Results:
1 |
|
连内部库的数组缓冲区都会被原样的复制!!!
example 3
多线程程序的某个线程执行 fork()
,应该发生什么?
- 这是个很有趣的问题:创造 fork 时创始人并没有考虑线程
我们可能作出以下设计:
- 仅有执行 fork 的线程被复制,其他线程 “卡死”
- 仅有执行 fork 的线程被复制,其他线程退出
- 所有的线程都被复制并继续执行
- 这三种设计分别会带来什么问题?
自己思考,后续会提到!
execve()
Fork: 程序是状态机,正在执行的程序是进程(正在运行的状态机),Fork是状态机的副本。
- 光有 fork 还不够,怎么 “执行别的程序”?-> Execve()
UNIX 的答案: execve
- 将当前运行的状态机重置成成另一个程序的初始状态
1 |
|
- 执行名为
pathname
的程序 - 允许对新状态机设置参数argv(v) 和环境变量envp(e)
- 刚好对应了
main()
的参数! - execve-demo.c
- 刚好对应了
1 |
|
同一个进程,同一个进程号,所有的资源都在。但是,状态机被重置。
Results:
1 |
|
printf没有打印出来昂!!!因为重置了状态机,所以原来状态机,所有的状态都没有了昂!!!
环境变量
“应用程序执行的环境”
- 使用env命令查看
PATH
: 可执行文件搜索路径PWD
: 当前路径HOME
: home 目录DISPLAY
: 图形输出PS1
: shell 的提示符
- export: 告诉 shell 在创建子进程时设置环境变量
- 小技巧:
export ARCH=x86_64-qemu
或export ARCH=native
- 上学期的
AM_HOME
终于破案了
- 小技巧:
可执行文件搜索路径
- 还记得 gcc 的 strace 结果吗?
1 |
|
- 这个搜索顺序恰好是
PATH
里指定的顺序
说白了,execve就会自动去我们的path里面去匹配,看看能不能找到,我们需要的东西!!!所以这里就和Windows里面一个道理,为啥需要配置Windows的环境变量(当下载软件的时候),这样软件才能找得到对应的软件的运行文件哇!!!
1 |
|
计算机系统里没有魔法。机器永远是对的。
_exit()
有了 fork, execve 我们就能自由执行任何程序了,最后只缺一个销毁状态机的函数!
UNIX 的答案: _exit
- 立即摧毁状态机
1 |
|
- 销毁当前状态机,并允许有一个返回值
- 子进程终止会通知父进程 (后续课程解释)
这个简单……
- 但问题来了:多线程程序怎么办?
exit 的几种写法 (它们是不同):
- exit(0) - stdlib.h中声明的 libc 函数
- 会调用
atexit
- 会调用
- _exit(0) - glibc 的 syscall wrapper
- 执行 “exit_group”系统调用终止整个进程 (所有线程)
- 细心的同学已经在 strace 中发现了
- 不会调用
atexit
- 执行 “exit_group”系统调用终止整个进程 (所有线程)
- syscall(SYS_exit, 0)
- 执行 “
exit
” 系统调用终止当前线程 - 不会调用
atexit
- 执行 “
atexit会帮助我们做libc的clean up!!!atexit其实就是一个注册回掉函数的地方!!!
小例子
结束当前进程执行的四种方式
return
,exit
,_exit
,syscall
- exit-demo.c
- 用 strace 观察程序的执行
1 |
|
- summary
- main的返回和libc函数exit,不仅会执行atexit,还会做一些结尾处理。例如把printf的buffer的内容,给最终打印出来,给清空!
- glibc函数_exit。操作系统是无情的状态机管理者,才不管我们的buffer有没有东西没打印,有没有atexit。操作系统调用来关闭的时候,直接摧毁整个状态机,进程和里面所有的线程,资源,全部结束掉。
- syscall(SYS_exit, 0),执行的是老的操作系统的调用,关闭的是当前的这一个线程。而_exit则是删除所有的线程。
- Linux默认执行的是删除退出是_exit,所有的线程才退出,这其实是一种比较安全的行为。
References
qemu帮助手册:http://linux.51yip.com/search/qemu,上网查一下应该很多这个版本,虚拟机的启动,和进入虚拟机后的调试,都要使用到昂!
tmux的使用:tmux select-window
gcc exit-demo.c -static,静态编译之后,strace会简单一点昂!!!(因为会省去运行的时候,动态链接相关的系统调用,好看一点。)