操作系统导论学习笔记(二)

进程

操作系统为正在运行的程序提供的抽象,就是进程。

操作系统也是一种应用程序,会使用数据结构保存进程相关的信息。比如保存正在运行的进程的一些附加信息,保存就绪进程列表,跟踪阻塞进程的信息,以便在合适的时机进行唤醒。进程列表这种数据结构,有时也会被称为 程序控制块 PCB Program Control Block

在任何时刻,都可以清点进程在读取和修改什么内容,机器的哪部分会对进程造成影响,我们称之为进程的机器状态(machine state)

机器状态包括:

  • 内存:程序执行的指令和读取及修改的数据,进程可访问的内存称为进程的地址空间
  • 通用寄存器
  • 一些特殊寄存器
    • PC 指针:程序正在执行的指令
    • 栈指针 stack pointer,帧指针 frame pointer 用于管理函数参数栈、局部变量和返回地址
  • I/O 信息: 程序访问的持久化存储设备

现代系统进程都会提供的 API

  • 创建:程序变成进程的过程
  • 销毁:如果程序不肯自己退出,操作系统提供了接口让用户结束进程
  • 等待:有时等待进程停止运行是有用的
  • 其他控制:除等待和销毁外的其他控制接口,比如暂停执行和恢复执行
  • 状态:查看进程状态

操作系统创建进程

  1. 从磁盘加载代码和静态数据(比如初始化变量)到内存中
    1. 尽早加载 eagerly load
    2. 惰性加载 lazily load 只加载执行到的片段,需要内存分页和交换机制支持
  2. 分配内存,提供给程序的运行时栈使用,也可能会使用参数(argc, argv)初始化栈
  3. 也可能会给程序分配堆内存
  4. 其他初始化任务,特别是 I/O 相关的
    1. 在 Unix 中,所有进程都默认有 3 个打开的文件描述符:标准输入、标准输出和错误输出
  5. 启动程序:通过跳转到 main() 例程,操作系统将 CPU 的控制权交到新创建的进程中,从而程序开始执行

进程的 3 种状态及相互转换

  • 运行:在处理器中执行指令
  • 就绪:进程已准备好执行,但是操作系统没有选择它在此时执行
  • 阻塞:常见的例子是进程向磁盘发起了 I/O 请求时,它会进入阻塞状态,其他进程可以使用处理器

进程状态转换

进程 API 简介

fork()

fork() 创建的子进程,是调用 fork() 接口的进程的一份拷贝,但是也有其特殊的地方,包括:

  • 地址空间(私有的内存)
  • 寄存器
  • PC 指针 等等

并且 fork() 创建的子进程,入口不是 main 函数,而是 fork() 返回处,好像自己调用了 fork() 函数,不过区别于父进程,子进程 fork() 返回值是 0, 父进程返回值是子进程的 PID

wait()/waitpid()

父进程可以通过调用 wait() 或 waitpid() 来延迟自己的执行,直到子进程结束执行,wait() 函数返回至父进程。

exec()

成功的 exec() 调用不会返回。执行 exec() 调用不会创建新的进程,而是把当前进程转化为另一个进程,包括:

  • 替换当前进程内存空间中的内容,包括代码段和静态变量
  • 重新初始化栈空间和堆空间
  • 重置寄存器和 PC 指针

创建进程的操作,分为 fork() 和 exec() 两个接口,因此在 fork() 和 exec() 之间可以执行其他操作,父进程可以为子进程执行一些准备环境的操作,比如设置环境变量等。

shell 是一个用户程序,它展示一个提示符并等待输入。收到一个命令(以及参数)后,大部分情况下,它会在文件系统中查找这个命令对应的可执行程序,调用 fork() 创建一个用来执行命令的子进程,然后调用 exec() 的某个变体执行命令,最后调用 wait() 等待命令执行完成。当子进程执行结束后,shell 从 wait() 函数返回,重新显示提示符,等待下一个命令的输入。

1
cat test1 > test2

重定向操作实现:shell 程序调用 fork() 创建子进程后,先将标准输出关闭,并打开 test2 文件描述符,然后通过 exec() 执行 cat 命令,这样输出就会到 test2 中。

Unix 的管道和重定向实现方式是类似的,关闭标准输出后,输出连接至内核中的管道(pipe, 其实是个队列),管道的另一端连接至另外一个命令的输入。从而可以实现很长的命令链。简单举例:

1
grep foo test.txt|wc -l

其他

除了 fork(), exec(), wait() 外,还有很多控制进程的系统调用。比如 kill() 用来向进程发送信号,方便起见,很多 Unix 系统为 kill() 系统调用发送的不同信号绑定了快捷键:

  • ctrl+c 发送 SIGINT 信号(通常会终止进程)
  • ctrl+z 发送 SIGTSTP 信号,通常是暂停进程,可以通过内置的 fg 命令唤起进程继续执行

Unix 系统有一套丰富的 信号子系统,提供了向进程传递外部事件的丰富基础设施,包括在单个进程中接收和处理信号的方法,以及向进程组发送信号。为了使用这套通信方式,进程需要调用 signal() 来捕获特定信号,这样当这个信号发给它时,进程会暂停当前程序执行并运行一段特定的代码来响应这个信号。

谁可以向进程发信号,谁不可以?一般系统会同时有多个人在使用,如果任何人可以向任何程序发出终止信号,是很危险的,因此引入了 user 用户的概念:通过提供密码等认证信息,用户登入系统,获得系统资源的使用权限范围,在这个范围内,用户具有进程的完全控制权,包括创建、暂停、终止等。