时分共享:允许资源由一个实体使用一小段时间,然后由另一个实体使用一小段时间,如此下去。
哪些部分对进程很重要:
- 内存:指令存储在内存中,读写的数据也存储在内存中,毫无疑问进程可以访问的内存对进程很重要。
- 寄存器:指令最终还是要操作寄存器,当前寄存器的状态对进程很重要。
- 特别是特殊的寄存器:
- 程序计数器(PC):程序当前执行哪个指令。
- 栈指针:管理函数参数栈和局部变量。
- 帧指针:管理返回地址。
- 程序打开文件的句柄。
程序如何转化为进程(粗略)
- 首先,将代码和静态数据加载到内存中。
- 为程序分配一些内存,用来存储栈和堆的数据。
- 为程序分配一些文件描述符。
- 转跳到程序入口,比如 main 函数处,程序开始执行。
代码就是状态,进程也是
进程可以处于以下三种状态之一:
- 运行:进程正在处理器上执行。
- 就绪:进程已经准备好运行,但操作系统选择不让其执行。
- 阻塞:进程执行了某项操作,在其完成之后才会运行,典型例子是IO请求。
graph LR
A[运行] -->|时间片耗尽| B[就绪]
B -->|调度选中| A
A -->|I/O请求| C[阻塞]
C -->|事件完成| B
UNIX 中进程的创建API
操作系统通常提供以下 API 用以管理进程:
- 创建:如何创建一个新的进程。
- 销毁:除了进程自行退出外,操作系统还允许强行销毁进程。
- 等待:允许等待进程停止允许。
- 状态:用来获取有关进程的信息。
fork()
定义与基本原理
- 功能:复制当前进程创建子进程
- 返回值:
- 父进程返回子进程PID
- 子进程返回0
- 执行特性:
- 父子进程共享代码段
- 采用写时复制机制管理内存
- 执行顺序由调度器决定
fork 被调用后,操作系统将创建一个新的进程,此时存在两个进程——子进程和父进程。同时,新的进程不会重新从 main 函数开始执行,而是继续从 fork 运行,就像二者都调用了 fork 一样。同时,fork 是完全复制原进程的每一个 byte,文件、信号……。
子进程初始时会继承父进程的地址空间(采用写时复制机制获得独立地址空间)、文件描述符、信号处理器、堆栈等执行上下文。但是,子进程和父进程的 fork 返回值不一样:
- 子进程将返回 0。
- 父进程将返回子进程的 PID。
由此,可以通过 fork 的返回值是否为 0 来区分当前进程是子进程还是父进程,进而执行不同的操作。当然,具体是子进程先运行还是父进程先运行是不确定的。
文件描述符与资源复制:父进程的文件描述符表也会被复制到子进程,但实际的文件表项是共享的。这意味着如果不加处理,父子进程会共享同一打开文件的状态(如文件偏移量)。
多线程环境下的 fork 注意事项:在一个多线程进程中调用 fork() 时,只复制调用线程,其它线程不会被复制。这可能会导致在子进程中存在未完成的临界区或锁,进而引起不可预期的行为。
僵尸进程
在《CSAPP》中提到如果一个进程已经终止执行但其退出状态迟迟未被父进程读取时会成为“僵尸进程”。
具体来说,当子进程已经通过 exit()终止时,内核依旧会保留该进程的部分信息直到父进程通过 wait()或 waitpid()读取。
那么,僵尸进程到底持有什么数据?
僵尸进程占用的资源:
- PID。
- 内核进程表项:保留进程的退出状态、资源使用统计信息等。
- 少量内核内存。
不占用的资源:
- 不占用内存:代码段、数据段、堆栈均已完全释放。
- 不占用 CPU:进程不再调度执行。
- 不占用文件描述符:所有文件、socket均已关闭。即使未手动调用
close()也如此。
wait()
定义与基本原理
- 功能:父进程阻塞等待子进程状态变化
- 状态类型:
- 子进程终止
- 子进程被信号停止/恢复
表面上 wait 的基本作用是让父进程执行到此时挂起,等待子进程退出。当然,有时子进程退出之前 wait 就已经返回了。
实际上,根据 man 手册,wait, waitpid, waitid 的实际作用是等待调用着进程的子进程状态改变。具体来说,有以下状态:
- 子进程终止。
- 子进程被信号停止。
- 子进程被信号恢复。
waitpid() 系统调用会暂停调用线程的执行,直到由 pid 参数指定的子进程状态改变。默认情况下,waitpid() 仅等待已终止的子进程,但此行为可以通过下面的选项参数进行修改。
pid:
- $<-1$ :等待进程组 ID为传入值的绝对值中,任意子进程退出。
- $==1$ :等待任何子进程退出。
- $==0$ :等待“进程组 ID 与调用 waitpid() 时相同”的所有子进程。
- $>0$ :等待进程 ID 等于该 pid 值的特定子进程。
exec()
定义与基本原理
- 功能:加载新程序覆盖当前进程空间
- 特性:
- 不创建新进程
- 成功调用后不返回原程序
- 参数通过
argv传递
exec 会从可执行文件中加载代码和静态数据,然后复写自己的代码段和数据段。因此,exec 不会新建一个进程,而是夺舍原进程,也因此 exec 成功运行后旧进程中的代码不会执行,毕竟已经消失了。
受限直接执行
通过分时共享来虚拟化 CPU 时,存在两个挑战:
- 性能:不增加系统开销的情况下实现虚拟化。
- 控制:在运行进程的同时,保证操作系统对 CPU 的控制权。
受限制的操作
直接执行性能更快,但如果希望控制进程对文件的访问权限,我们必须处理进程向磁盘发送的 IO 操作。为了控制进程的权限,我们引入了两种模式:
- 用户模式:代码运行受限,例如进程不能直接发送 IO 请求。
- 内核模式:代码可以任意做想做的事。
在进程间切换
当进程在 CPU 上运行时,操作系统的代码并没有运行。但如果操作系统没有运行,如何控制进程切换?也就是说,存在一个关键问题:操作系统如何重新获得 CPU 控制权。
为此,计算机引入了时钟中断。时钟每隔几秒发生一次中断,中断时当前进程停止,操作系统设置的中断处理程序会让操作系统重新获得 CPU 控制权。