在正式开始这一章的介绍之前,我们可以看到:在前面的章节中基本涵盖了一个功能相对完善的操作系统内核所需的核心硬件机制:中断与异常、特权级、内存动态分配,而且一个一个逐步进化的操作系统内核让应用程序在开发和运行方面也越来越便捷和安全了。但开发者的需求是无穷的,开发者希望能够在计算机上有更多的动态交互和控制能力,比如在操作系统启动后,能灵活选择执行某个程序。但我们目前实现的这些操作系统还无法做到,这说明操作系统还缺少对应用程序动态执行的灵活性和交互性的支持!
到目前为止,操作系统启动后,能运行完它管理所有的应用程序。但在整个执行过程中,应用程序是被动地被操作系统加载运行,开发者与操作系统之间没有交互,开发者与应用程序之间没有交互,应用程序不能控制其它应用的执行。这使得开发者不能灵活地选择执行某个程序。为了方便开发者灵活执行程序,本章要完成的操作系统的核心目标是: 让开发者能够控制程序的运行 。
目前为止,所有的应用都是在内核初始化阶段被一并加载到内存中的,之后也无法对应用的执行进行动态增删。事实上,由于我们还没有充分发掘这些硬件机制和抽象概念的能力,应用的开发和使用仍然比较受限,且用户在应用运行过程中的动态控制能力不够强。其实用户可以与操作系统之间可以建立一个交互界面,在应用程序的执行过程中,让用户可以通过这个界面主动给操作系统发出请求,来创建并执行新的应用程序,暂停或停止应用程序的执行等。
于是,本章我们会开发一个用户 终端 (Terminal) 程序或称 命令行 应用(Command Line Application, 俗称 shell ),形成用户与操作系统进行交互的命令行界面(Command Line Interface),它就和我们今天常用的 OS 中的命令行应用(如 Linux 中的 bash,Windows 中的 CMD 等)没有什么不同:只需在其中输入命令即可启动或杀死应用,或者监控系统的运行状况。这自然是现代 OS 中不可缺少的一部分,并大大增加了系统的 可交互性 ,使得用户可以更加灵活地控制系统。
UNIX shell 的起源
“shell” 的名字和概念是从 UNIX 的前身 MULTICS 发展和继承过来的,应用程序可以通过 shell 程序来进行调用并被操作系统执行。Thompson shell 是历史上第一个 UNIX shell,在 1971 年由肯·汤普逊(Ken Thompson)写出了第一版并加入 UNIX 之中。Thompson shell 按照极简主义设计,语法非常简单,是一个简单的命令行解释器。它的许多特征影响了以后的操作系统命令行界面的发展。至 Version 7 Unix 之后,被 Bourne shell 取代。
为了在用户态就可以借助操作系统的服务动态灵活地管理和控制应用的执行,我们需要在已有的 任务 抽象的基础上进一步扩展,形成新的抽象: 进程 ,并实现若干基于 进程 的强大系统调用。
- 创建 (Create):父进程创建新的子进程。用户在 shell 中键入命令或用鼠标双击应用程序图标(这需要 GUI 界面,目前我们还没有实现)时,会调用操作系统服务来创建新进程,运行指定的程序。
- 销毁 (Destroy):进程退出。进程会在运行完成后可自行退出,但还需要其他进程(如创建这些进程的父进程)来回收这些进程最后的资源,并销毁这些进程。
- 等待 (Wait):父进程等待子进程退出。父进程等待子进程停止是很有用的,比如上面提到的收集子进程的退出信息,回收退出的子进程占用的剩余资源等。
- 信息 (Info):获取进程的状态信息:操作系统也可提供有关进程的身份和状态等进程信息,例如进程的ID,进程的运行状态,进程的优先级等。
- 其他 (Other):其他的进程控制服务。例如,让一个进程能够杀死另外一个进程,暂停进程(停止运行一段时间),恢复进程(继续运行)等。
有了上述灵活强大的进程管理功能,就可以实现本章具有进程管理功能的操作系统了。
任务和进程的关系与区别
第三章提到的 任务 和这里提到的 进程 有何关系和区别? 这需要从二者对资源的占用和执行的过程这两个方面来进行分析。
- 相同点:站在一般用户和应用程序的角度看,任务和进程都表示运行的程序。站在操作系统的角度看,任务和进程都表示为一个程序的执行过程。二者都能够被操作系统打断并通过切换来分时占用 CPU 资源;都需要 地址空间 来放置代码和数据;都有从开始到结束运行这样的生命周期。
- 不同点:第三/四章提到的 任务 是这里提到的 进程 的初级阶段,任务还没进化到拥有更强大的动态变化功能:进程可以在运行的过程中,创建 子进程 、 用新的 程序 内容覆盖已有的 程序 内容。这种动态变化的功能可让程序在运行过程中动态使用更多的物理或虚拟的 资源 。
在课程仓库的 code/lab7
目录下:
$ cd os
# 构建并运行代码
$ make run -j $(nproc)
待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入 Shell 程序:
...
/**** APPs ****
exit
fantastic_text
forkexec
forktest
forktest2
forktest_simple
forktree
hello_world
initproc
matrix
sleep
sleep_simple
stack_overflow
user_shell
usertests
usertests-simple
yield
**************/
Rust user shell
其中 usertests
打包了很多应用,只要执行它就能够自动执行一系列应用。
只需输入应用的名称并回车即可在系统中执行该应用。以应用 exit
为例:
>> exit
I am the parent. Forking the child...
I am parent, fork a child pid 3
I am the parent, waiting now..
I am the child.
waitpid 3 ok.
exit pass.
Shell: Process 2 exited with code 0
>>
当应用执行完毕后,将继续回到 Shell 程序的命令输入模式。