旧游无处不堪寻
无寻处,惟有少年心
操作系统(二)

进程 API


UNIX 系统通过一对系统调用: fork()和 exec() 来创建进程。还可以通过 wait() 系统调用,来等
待其创建的子进程执行完成。

fork 系统调用

系统调用 fork() 用于创建新进程,不同于一般的函数,其返回两次。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // parent goes down this path (main)
printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
}
return 0;
}

// 输出如下:
// hello world (pid:29146)
// hello, I am parent of 29147 (pid:29146)
// hello, I am child (pid:29147)
// 或者
// hello world (pid:29146)
// hello, I am child (pid:29147)
// hello, I am parent of 29147 (pid:29146)

父进程获得的返回值是新创建子进程的 PID,而子进程获得的返回值是 0。它的输出顺序是不确定的,子进程可能先运行也可能是父进程先运行。CPU 调度程序(scheduler)决定了某一时刻哪个进程被执行。

wait 系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // parent goes down this path (main)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
}
return 0;
}
// 输出如下:
// hello world (pid:29266)
// hello, I am child (pid:29267)
// hello, I am parent of 29267 (wc:29267) (pid:29266)

父进程调用 wait() 系统调用,延迟自己的执行直到子进程执行完毕。当子进程结束时,wait() 函数才返回。
那么,上述例子的输出结果就是确定的。

exec 系统调用

exec() 系统调用可以让子进程执行与父进程不同的程序。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
char *myargs[3];
myargs[0] = strdup("wc"); // program: "wc" (word count)
myargs[1] = strdup("p3.c"); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
printf("this shouldn't print out");
} else { // parent goes down this path (main)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
}
return 0;
}

// 输出如下:
// hello world (pid:29383)
// hello, I am child (pid:29384)
// 29 107 1030 p3.c
// hello, I am parent of 29384 (wc:29384) (pid:29383)

其他系统调用

除了 fork()、exec() 和 wait() 之外,UNIX 中还有其他许多系统调用比如可以通过 kill() 系统调用向进程发送信号(signal)。

进程之间切换


协作式多任务切换

过去某些系统采用协作(cooperative)方式来进行进程切换。在这种方式下,操作系统相信运行的进程会合理运行,进程会定期放弃 CPU,以便操作系统可以决定运行其他任务。
多数进程通过进行系统调用,将 CPU 的控制权转移给操作系统。也就是说在协作调度系统中,操作系统通过等待系统调用或某种非法操作发生,从而重新获得 CPU 的控制权。

抢占式多任务切换

如果没有硬件的额外帮助,如果进程拒绝进行系统调用也不出错,从而将控制权交还给操作系统,那么操作系统永远无法再次获得 CPU 的控制权。在协作式多任务切换中,当进程陷入无限循环时,唯一的办法就是重启。为了解决这个问题,就产生了抢占式(preemptive)方式进行进程切换。

如何在没有协作的情况下获得控制权呢,时钟中断(timer interrupt)可以解决这个棘手的问题。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统会重新获得 CPU 的控制权。

保存/恢复上下文

操作系统重新获得控制权后,都必须决定是继续运行当前正在运行的进程,还是切换到另一个进程。这个决定是由调度程序(scheduler)决定的。
如果决定进行切换,操作系统就会执行一些底层代码,即上下文切换(context switch)。
对于上下文切换,指的是操作系统为当前正在执行的进程保存一些寄存器的值到它的内核栈,并为即将执行的进程从它的内核栈恢复一些寄存器的值。

进程调度


前面我们一直在说操作系统通过调度来决定执行哪个进程。那么调度程序采用的具体策略(policy)是什么呢,接下来会介绍一系列的调度策略(sheduling policy)。

  1. 先进先出(First In First Out,FIFO)
  2. 最短任务优先(Shortest Job First,SJF)
  3. 最短完成时间优先(Shortest Time-to-Completion First,STCF)
  4. 轮转调度(Round-Robin,RR)
  5. 多级反馈队列(Multi-level Feedback Queue,MLFQ)
  6. 比例份额调度(proportional-share)