OPERATING SYSTEM - PROCESS 进程

最近节日大爆发,感恩节接着圣诞节,再接着元旦,除了跨年外实在是没有事情做,索性就学习好了。操作系统是一直以来想看的内容,自从大三的时候操作系统课拿了个D,就一直想什么时候重新学一下。好吧那就不要拖延的看起来吧。选择的课本是Abraham Silberschatz的经典之作“Operating System Concepts (10th Edition)”。本篇学习笔记出自于这本书的第三章“Process”。如果有什么地方理解不到位,或者写的不清楚欢迎留言指出,一起进步。

什么是Process?

一个Process一般代表计算机的一项任务或一项活动,它并不是程序本身,而是程序加上程序现在处于的状态。一个process的memory可以分成以下几个section:

  • Text表示一些固定长度的只读文本。
  • Data分为初始化的data和未初始化的data(bss),比如在C里的 $int$ $a = 3;$和$int$ $a;$。
  • Heap和stack都是flexible length的,如图所示heap是向高地址扩展的,而stack是向低地址扩展的。当你动态allocate一些memory的时候,其实获取的是heap的memory,在C里就是malloc函数族。
  • Stack的作用是存储一些临时变量,比如在调用一个函数的时候,输入,本地变量,还有返回地址会被push到stack上,直到函数调用完成后这一个block才被pop出来。

当然heap和stack有可能会无止境增长直到重叠,在现在操作系统里一般都会有机制防止这种情况发生。在stack即将和heap重叠的时候可能会报stack overflow的错。在heap空间不够的时候malloc会直接返回NULL告诉你heap空间不够了。

这里的重点是,程序不是process。如果一个程序需要完成多项工作,它可以produce很多进程。

Process的状态

  • New Process刚被创建
  • Ready Process正在等待被分配运行(通常有一个scheduler来分配)
  • Running Process正在被执行
  • Waiting 相当于暂停,进程等待某个事件的发生(可能是某种signal,或者是OS把运行权限交回给这个process)
  • Terminated Process完成工作,等待OS回收
    一般,处理器有几个core,就能真正同时运行几个process。比如6-core的处理器能真正同时处理6个process。在每个process core里,还能够伪同步地处理多个process,这是因为OS的scheduler会不停的context switch,在多个process中切换,造成多个process同时运行的假象。

Process Control Block

这是一个拥有process各种metadata的字节块。它包括:

  • Process State
  • Program Counter 这个进程下一步需要执行的指令所在的位置,他会根据process的运行动态更新。
  • CPU Register 与这个process相关的寄存器里的值
  • CPU Schedueling Info, Memory Management Info, Stats, I/O Info 与这个process相关的Scheduler, Memory manager, 运行状态和I/O状态的信息
    其实所有的内容还是为了当这个process重新运行的时候恢复到上次运行的状态而储存的。

Process调度

为什么要做process的调度?因为process会因为各种原因运行很长的时间,比如需要计算很多东西(Computing Bound),或者需要做很多I/O工作(I/O Bound),或者在等子进程完成他们的任务。为了不浪费CPU的资源,processer需要在多个process之间进行切换,防止有空闲的时间。

一般来说,OS会维持两个queue。其中一个是ready queue,是一些运行的时候被interrupt的process,他们随时随地都能被re-run。还有一个是wait queue,他们是本身在等待某些signal的process。当它们收到signal之后,会被放进ready queue。

CPU Scheduler的任务是在ready queue里选出一个process,并把他assign给某个core去运行。为了保证某些空闲的process不占用CPU,scheduler会很频繁的运行,即使会打断一些在正常运行的process。打断进程和恢复进程是通过context switch进行的,简单来说就是先把当前process的所有状态存到它的PCB里去,然后再resume另一个进程的PCB。Context switch是一个很纯粹的overhead,他除了调度之外不做其他的事情,并且它的速度取决于硬件。

创建一个Process

一个Process是由其他的Process创建出来的,这个被创建的Process叫做子进程,它是由它的parent process创建出来的。在计算机boot的时候,OS会创建它的第一个process——init(pid=1)。它是所有进程的父进程。所以计算机里所有process的关系就组成了一个tree。每一个process会有自己的一个唯一的pid(process id)。

当进程创建自己的子进程的时候,子进程可以从OS那里拿到自己份的资源,也可以是从自己的父进程那里分一部分的资源。(这是OS层面的一个实现决策。)像后者的话可以防止一个进程创建出太多的子进程而占用太多资源。

当子进程被创建之后,它会复制一份父进程的地址空间。这里指的是两个进程的memory layout以及里面的数据是完全一样的,只是因为他们是虚拟地址空间,他们会指向实体memory里的不同位置。一般来说当子进程被创建后,它可以做和父进程完全一样的任务,或者可以做自己的新的任务。而父进程的话可以等待子进程的完成然后再terminate自己,或者也可以继续做事。

fork()函数是用来创建子进程的函数,它很神奇,在于它有两个返回值。由于调用fork之后子进程已经被创建了,这个函数的返回值其实是被两个进程可见的,但是它在两个进程的返回值是不一样的。它在子进程里返回0,在父进程里返回子进程的pid。

int main() {  
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    } else if (pid == 0) {
        execlp("/bin/ls", "ls", NULL);
    } else {
        wait(NULL);
        printf("Child Process Complete.");
    }

    return 0;
}

先看一下上面这个例子,父进程调用fork之后,两个进程一起进入if判断。pid == 0的那个分支会被子进程执行,因为fork在子进程里返回值为0。这里子进程是直接执行了一个新的任务了。else的那个分支会被父进程执行,因为子进程的pid永远是一个正数,父进程的做法是挂停,并且等待子进程的完成(查看wait函数)。父进程会在子进程结束后收到signal,并且重新执行下去。再看一个例子:

int main() {
    fork();
    fork();
    fork();

    return 0;
}

那么包含当前进程,现在一共有多少进程在run? 答案是8个。

结束一个Process

当一个Process结束它的任务时,它通过调用exit函数实现system call,从而达成资源的回收。一个Process也可以被另一个Process所terminate,出于安全的考虑,一般只有它的父进程有这样的权限。父进程强行terminate它的子进程一般可以是因为,子进程占用了太多的资源,子进程的任务不再需要被完成了,或者父进程自己即将被terminate了。一般来说,父进程需要等其子进程都被terminate了,自己才能terminate。

如果父进程在子进程完成之前被terminate了,我们称子进程为orphan process。这种情况下,通常由init进程来通过wait函数把他kill掉。还有一个概念叫做zombie process。由于子进程结束的时候,它假设它的父进程还在运行中,并且最终会检查且kill掉自己。为了使父进程能够知道自己的状态,它必须还将自己的metadata维持在process table中。这样一个不再工作的,却等待父进程来kill掉自己的进程叫做zombie process。

IPC - Interprocess Communication 进程间通信

什么情况下需要IPC?比如需要同时(不出错的)访问某一块地址上的数据,比如需要一起完成某一项大任务。IPC能够允许两个进程之间交换数据,更具体地说,发送和接受消息。具体说来,IPC的实现主要有shared memory和message passing两种方法。

Shared memory主要通过建立一块两个进程的公共内存区域。这种方法会使IPC比较快速,但是增加了实现的难度。如果两个进程都同意他们即将共享某一个内存区域,他们就可以这么做,同时他们应该做好concurrency的管理工作。POXIS系统里提供了这种IPC方法的API。

Message passing主要指两个进程之间互相收发信息,这种方法一般比较好实现,也不需要做concurrency的管理,所以分布式系统比较会用这种方法。如果两个进程可以互相传送信息的话,逻辑层面上他们之间需要有一个link。关于这个link,设计的时候可以有以下的几种option:

Direct or Indirect 在每个process想和别的process进行通信的时候,需要严格说明对方的名称,或者只有sender需要说明对方的名称。这种精确到名字的link一般不太好使用,一旦process的id被update了,这种方法的overhead会非常高。Indirect的link的话,有port做过度。这样的话IPC就不是直接进程对进程,而是两组进程对port。port可以是OS层面的,也可以是进程层面的。
Synchronization or Asynchronization 是不是允许sender和receiver 被block?
Buffering 是不是允许sender和receiver有一个queue来存储一系列的消息?还是只能够一条一条的发送和接收?
Pipe是一种message passing的IPC方法。一个pipe有两端,每一端都是一个file descriptor,一端作为读口,一端作为写口。原始的pipe允许一个进程从其中一端写入,允许另一个进程从另外一端读取。一般来说,原始的pipe的使用场景在于父进程和子进程之间的IPC。首先,父进程创建一个新的pipe,然后调用fork函数创建子进程。这时父进程和子进程同时都有相同的pipe。之后分别关闭掉父进程和子进程中不用的一端,即可完成父进程和子进程之间的写入读取。当然pipe的设计可以有多种option,比如是不是只允许父子进程的通信,或者允许单方向还是双方向。