关于进程

176 阅读5分钟

关于进程的概念及使用,看了很多书,但感觉总是记不牢,这里总结一下。
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,操作系统提供了一种假象,好像每个进程都在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。进程是计算机科学中最重要和最成功的概念之一。
通俗一点来说,若我现在只有一个CPU,某一时刻只能运行一个进程比如qq,但是我还想边聊qq边听音乐,这时就需要进程切换,CPU处理一段时间qq,处理一段时间音乐,循环往复,只是这种切换的过程很快,我们感知不到,也就实现了感观上的多个进程同时运行。对于计算机来说进程切换的过程就涉及到进程的状态。进程的状态可分为三态模型和五态模型:(红色标出的为五态较三态多出的两个模型)

管理员用例图.png 进程是按照箭头顺序进行状态转换,因而不会出现阻塞态直接到运行态。(补完车票仍需排队上车)
了解完概念之后,看一下如何编程:(Linux平台)

创建进程

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

函数原型fork创建一个子进程,函数返回值在父进程中返回子进程的PID(PID唯一标识一个进程),在子进程中返回0,fork调用失败返回-1。

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

int main() {
    pid_t pid;
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        printf("I'm child\n");
    } else {
        printf("I'm parent\n");
    }
    return 0;
}

创建后的父子进程有独立的内存空间,fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有很多属性被赋予了新的值,比如子进程的PPID为父进程的PID,原进程设置的信号处理函数不再对新进程起作用,子进程中资源统计信息会清零,所有文件锁也都不会被子进程所继承(详细查看man fork)。
子进程的代码与父进程完全相同,还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用写时复制(copy on write),只有任一进程(父进程或子进程)对数据执行了写操作,复制才会发生。
创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数均会加1。

结束进程

#include <stdlib.h>
void exit(int status);

参数status用于标识进程的退出状态,0表示成功,非0,如1或-1,表示失败。
如果子进程在父进程之前结束,内核应该把该子进程设置为特殊的进程状态。这种状态的进程称为僵尸进程。僵尸进程只保留最小的概要信息(一些基本内核数据结构),保存可能有用的信息。僵尸进程会等待父进程来查询自己的状态。只有当父进程获取到了已终止的子进程的信息,这个子进程才会正式消失,不再处于僵尸状态。

等待子进程结束

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

父进程调用wait查询子进程状态,调用wait()成功后,会返回已终止子进程的pid,出错时,返回-1。如果没有子进程终止,调用会阻塞,直到有一个子进程终止。如果有一个子进程已经终止了,调用会立即返回。
当一个进程有很多子进程,我们只需等待其中一个子进程结束的情况下,可以使用waitpid

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

pid可以指定要等待的子进程
如果pid的值为-1,会等待任何一个子进程退出,行为和wait一致:

//以下两种效果完全一样:
wait(&status);
waitpid(-1, &status, 0);

僵尸进程会消耗系统资源,拖累系统。因此,如果进程创建了一个子进程,那么它就有责任去等待子进程结束。
子进程先于父进程结束会变为僵尸进程,而父进程先于子进程结束,子进程就会变为孤儿进程,被1号进程收养。

获取进程ID

#include <sys/types.h>
#include <unistd.h>
//返回该进程ID
pid_t getpid(void);
//返回该进程的父进程ID
pid_t getppid(void);

通常情况下我们创建一个子进程是为了让它执行其他程序,这时就用到exec族函数

exec系统调用

int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., charconst envp[]);
int execv(const char* path, charconst argv[]);
int execvp(const char* file, charconst argv[]);
int execve(const char* path, charconst argv[], charconst envp[]);

l和v分别表示参数是以列表方式还是数组方式提供的。path参数指定可执行文件的完整路径,带p的函数表示会在用户的绝对路径写查找可执行文件,可以只指定文件名,该文件必须在用户路径下。e会为新进程提供新的环境变量。

实例:父进程创建一个子进程,子进程打开一个temp.txt文件并写入内容,父进程等待子进程结束,并读取temp.txt内容

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

int main() {

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
        //child process
        execl("/usr/bin/vim""vim""temp.txt"NULL);
        printf("hello ? \n");//代码被替换,不会执行
    } else {
        //father process
        wait(NULL);
        execlp("cat""cat""temp.txt"NULL);
    }
    return 0;
}

原程序会被exec的参数指定的程序完全替换(包括代码和数据)

以上是进程相关的一些函数。
下面来看两个小例子:

一、猜一下这段程序输出什么?

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

int main() {
    pid_t pid;
    printf("I'm Father!");
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        printf("I'm child\n");
    } else {
        printf("I'm parent\n");
    }

    return 0;
}

输出:

I'm Father!I'm parent
I'm Father!I'm child

这里涉及一个概念行缓存机制
printf行缓存机制,在打印I'm Father!时,不满一行所以先不输出,之后子进程拷贝父进程的缓存,因此父子进程最后都输出了I'mFather!,如果改为printf("I'm Father!\n");,则只输出一次。

二、当前需要创建100个子进程

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

int main() {
    pid_t pid;
    for (int i = 1; i <= 100; i++) {
        if ((pid = fork()) < 0) {
            perror("fork");
            exit(1);
        }
    }
    return 0;
}

可以跑一下这段程序,会发现电脑可能就崩了。这里需要注意,要创建100个子进程,我们需要保证每个子进程都是由同一个父进程创建的
上述程序没有明确区分,那么会导致新创建的子进程继续创建新的子进程,一直套娃下去。。。
区分父子空间也是在多进程编程中需要时刻注意的事。
正确写法应该是:

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

int main() {
    pid_t pid;
    int x;
    for (int i = 1; i <= 100; i++) {
        x = i;
        if ((pid = fork()) < 0) {
            perror("fork");
            exit(1);
        }
        //如果是刚创建的子进程就退出循环
        if (pid == 0) {
            break;
        }
    }
    if (pid == 0) {
        printf("I'm %dth Child!\n", x);
        sleep(1);
    } else {
        for (int i = 1; i <= 100; i++) {
            wait(NULL);
        }
    }
    return 0;
}

进程相关的东西还有很多,像守护进程、进程组、高级进程管理等等。之后再写,下一篇总结进程间通信。

参考资料
[1] 游双.Linux高性能服务器编程[M].北京:机械工业出版社,2013:239-240.
[2] 龚奕利,贺莲译.深入理解计算机系统[M].北京:机械工业出版社,2016.
[3] 祝洪凯,李妹芳,付途译.Linux系统编程[M].北京:人民邮电出版社,2014.