Linux系统编程 day05 进程控制
- 1. 进程相关概念
- 2. 创建进程
- 3. exec函数族
- 4. 进程回收
1. 进程相关概念
程序就是编译好的二进制文件,在磁盘上,占用磁盘空间。程序是一个静态的概念。进程即使启动了的程序,进程会占用系统资源,如内存、CPU等,是一个动态的概念。
在一个时间段内,如果在同一个CPU上运行了多个程序,这就叫并发。在同一个时刻,如果CPU中运行了两个以及两个以上的程序,就叫并行,并行要求计算机要有多核CPU。
计算机的每一个进程中都有一个进程控制块(PCB)来维护进程的相关信息,Linux中的进程控制块是task_struct
结构体。每一个进程都有一个唯一的ID,在C语言中用pid_t
表示一个非负整数。每一个进程都有自己的状态,进程的状态有创建态、就绪态、运行态、挂起态、终止态等。在CPU发生进程切换的时候,PCB需要保存和恢复一些CPU寄存器的信息。
创建态就是进程刚创建的一个状态,随后会进入就绪态,一般常将就绪态和创建态结合着看。当就绪态的进程得到CPU的执行权分得时间片的时候,就会进入运行态,当时间片消耗完则会继续进入就绪态。在运行态的进程,如果遇到了sleep命令等就会进入挂起态,当sleep结束之后就会继续进入就绪态。就绪态的进程也会因为受到SIGSTOP信号而进入到挂起态。就绪态、运行态、挂起态三个状态的进程都有可能随时进入终止态,结束程序的运行。需要值得注意的是挂起态不能直接转到运行态,必须先转为就绪态。
2. 创建进程
在Linux中创建子进程我们使用fork
函数。该函数的原型为:
#include <sys/types.h>#include <unistd.h>pid_t fork(void); // 创建一个子进程
其中该函数需要sys/types.h
和unistd.h
这两个头文件。该函数不需要任何参数,返回值为子进程的pid,若失败了则会返回-1
。经过该函数之后,我们可以得到两个pid,不是因为fork
的返回值为两个,而是有两个进程在调用fork
函数。因为我们创建了一个子进程,而本来就有一个进程。父进程调用fork
函数会返回子进程的pid,而子进程调用fork
函数返回的是0。所以在我们可以通过判断fork
函数的返回值来确定究竟是子进程还是父进程。若pid小于0,则表明子进程创建失败,若pid等于0则说明该进程是子进程,若pid大于0则说明是父进程(返回的是子进程的pid)。
在创建的子进程的时候,操作系统会拷贝一份父进程的内存,内存分为内核区和用户区,其中用户区的数据内容是完全一样的,而内核区的内容不完全一样。比如pid就在内核区,因为每个进程都使用pid作为进程的唯一标识,所以不能一样。
在子进程创建了之后,父进程执行到了什么位置,子进程就会继续从该位置继续执行。两者的执行顺序并不一定是父进程就优先比子进程执行,也不是子进程一定优先比父进程执行,而是谁先抢到CPU的时间片谁就优先执行。
如何获得该进程的进程pid呢?操作系统为我们提供了两个函数。
#include <sys/types.h>#include <unistd.h>pid_t getpid(void); // 获取当前运行进程的pidpid_t getppid(void); // 获取当前进程的父进程的pid
这两个函数第一个函数getpid
是用于获取当前运行的进程的pid。而getppid
是用于获取当前运行的进程的父进程的pid。
接下来我们来看一个使用fork
函数的例子。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("Before fork, pid: [%d]\n", getpid());//pid_t fork(void);// 创建子进程pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){// 子进程printf("child: pid: [%d], fpid: [%d]\n", getpid(), getppid());}else{// 父进程sleep(2);printf("father: pid: [%d], fpid: [%d]\n", getpid(), getppid());}printf("After fork, pid: [%d]\n", getpid());return 0;
}
前面我们说了在创建子进程的时候会拷贝一份父进程的内存,那么它们共享全局变量这些吗?实际上在多进程的程序中,它们做的是读时共享,写时复制。意思就是在不对内存的数据进行修改的时候它们是共享的,但是当你修改数据的时候操作系统会复制一份新的内存映射回去,再对这块复制的内存进行修改操作。所以父子进程不能共享全局变量。下面程序就验证了这个特性。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int g_var = 100;int main()
{// 创建子进程pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid > 0){// 父进程printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());g_var ++;printf("father: g_var = [%d], addr = [%p]\n", g_var, &g_var);}else{// 子进程sleep(1); // 避免父进程还没有执行子进程就已经结束printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());printf("child: g_avr = [%d], addr = [%p]\n", g_var, &g_var);}return 0;
}
现在有一个问题,假如现在我要创建n个子进程,又应该怎么创建呢?假如是3个,那么我能否使用以下的语句进行创建呢?
for(int i = 0; i < 3; i ++)
{pid_t pid = fork();
}
从代码的表面上来看,的确是创建了3个子进程,实际上这里创建的远远不止3个子进程。分析以下原因是因为每一个子进程都会去执行fork
函数。假如我们把父进程记为p0,当i=0
时,p0会创建一个子进程p1。当i=1
时,p0会创建子进程p2,p1会创建它的子进程p3。当i=2
时,p0、p1、p2、p3都会分别创建一个子进程。综上,我们可以得出这里一共创建了7个子进程。也就是循环n次就会创建 2 n − 1 2^n-1 2n−1个子进程。那么又如何该完成我们创建n个进程的任务呢?用循环是肯定的,但是我们在每次创建的时候都可以让子进程跳出循环,避免子进程创建新的子进程,让父进程一直循环创建。也即是在创建子进程之后,我们需要在子进程的运行代码中使用break
语句。例如创建4个子进程,代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>// 循环创建n个进程
int main()
{int i = 0;for(i = 0; i < 4; i ++){pid_t pid = fork();if(pid < 0){// 创建失败perror("fork error");return -1;}else if(pid > 0){// 父进程printf("father--pid:[%d]--fpid:[%d]\n", getpid(), getppid());}else {// 子进程printf("child--pid:[%d]--fpid:[%d]\n", getpid(), getppid());break;}}if(i != 4){printf("[%d]--pid:[%d]--fpid:[%d]---child\n", i, getpid(), getppid());}else{printf("[%d]--pid:[%d]--fpid:[%d]---father\n", i, getpid(), getppid());}sleep(4);return 0;
}
在Linux的shell中,我们常用ps
去查看当前还在运行的进程,以及用kill
去杀死某个进程。在ps
中,常用的参数由以下四个。
参数 | 作用 |
---|---|
-a | 当前系统的所有用户进程 |
-e | 当前系统的所有进程,作用与-a一样 |
-f | 按照完整格式列表显示 |
-u | 查看进程所有者以及其它一些信息 |
-x | 显示没有控制终端的进程,也就是不能与用户进行交互的进程 |
-j | 列出与作业控制相关的信息 |
在kill
中,我们会使用-9
或者-15
去杀死某个进程,这里的数字是一些信号。在Linux中的信号有以下:
3. exec函数族
有时候我们需要一个进程里面去执行其它的命令或者是用户的自定义程序,这个时候就需要我们使用exec函数族中的函数。使用的一般方法都是先在父进程中创建子进程,然后在子进程中调用exec函数。exec函数族的常用函数原型如下:
#include <unistd.h>int execl(const char *pathname, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);
这些函数的作用是在子进程中在执行自定义应用程序或者命令。
函数名 | 参数 | 返回值 | 作用 |
---|---|---|---|
execl | pathname:文件路径名 arg:占位参数 …:程序的外部参数 | 成功不返回,失败返回-1并设置errno | 在子进程中执行路径pathname指定的程序 |
execlp | file:文件名 arg:占位参数 …:程序的外部参数 | 成功不返回,失败返回-1并设置errno | 在子进程中执行file文件 |
上面两个exec函数中的第二个参数arg
是占位参数,一般写成和第一个参数一样的,这个参数的作用在于使用ps
查询进程的时候可以看到进程名为arg
的值。后面的...
为执行的外部参数,比如我们使用ls
命令的时候需要按照时间顺序逆序排序则需要写成-ltr
。在...
写完之后,必须写上一个NULL
表示参数结束。
一般我们使用execl
函数来执行自定义应用程序,而使用execlp
来执行内部的命令。使用execlp
的时候,第一个file
参数会根据系统的PATH
变量的值来进行搜索。
使用execl
的示例如下:
#include <stdio.h>
#include <unistd.h>int main()
{pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){execl("helloworld", "helloworld", NULL);}else if(pid > 0){printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());sleep(20);}return 0;
}
使用execlp
的示例如下:
#include <stdio.h>
#include <unistd.h>int main()
{pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){sleep(4);printf("child --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());execlp("ls", "ls", "-ltr", NULL);}else{sleep(10);printf("father --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());}return 0;
}
4. 进程回收
当一个进程退出之后,进程能够回收自己用户区的资源,但是不能回收内核空间的PCB资源,这个必须要它的父进程调用wait
或者是waitpid
函数完成对子进程的回收,避免造成系统资源的浪费。这两个函数在后面会进行介绍。
在一个程序中,如果父进程已经死了,而子进程还活着,那么这个子进程就成为了孤儿进程。为了保证每一个进程都有一个父进程,孤儿进程会被init
进程领养,init
进程就会成为子进程的养父进程,当孤儿进程退出之后,由init
程序完成对孤儿进程的回收。需要注意的是在某些使用Systemd
来管理系统的Ubuntu上就可能是由systemd
进程来收养,而不是init
进程。如下面就是一个孤儿进程的案例。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid > 0){sleep(3);printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());}else{printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());sleep(10);printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());}return 0;
}
如果在一个程序中,子进程已经死了,而父进程还活着,但是父进程没有用wait
或者waitpid
对子进程进行回收,则这个子进程就会称为僵尸进程。
僵尸进程在用ps
进行查询的时候会有<defunct>
标识。由于僵尸进程是一个已经死亡了的进程,所以我们不能使用kill
进行杀死,那么怎么解决僵尸进程的问题呢?
第一个解决方法是将它的父进程给杀死,因为父进程死亡之后僵尸进程会被init
进程所领养,然后被init
进程回收其资源。第二个方法就是在父进程中调用wait
或者waitpid
函数进行回收子进程的资源。这两个函数的原型如下:
#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *wstatus);pid_t waitpid(pid_t pid, int *wstatus, int options);
函数名 | 参数 | 返回值 | 作用 |
---|---|---|---|
wait | wstatus:子进程的退出状态 | 成功返回清理掉的子进程pid,失败返回-1(没有子进程) | 阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因 |
waipid | pid:需要回收的进程pid wstatus:子进程的退出状态 option:阻塞或者非阻塞,设置WNOHANG为非阻塞,设置为0表示阻塞 | 返回值大于0表示回收掉的子进程的pid,返回值为-1表示没有子进程,返回值为0且option为WNOHANG的时候表示子进程正在运行 | 阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因 |
在上面的waitpid
函数中,若pid=-1
表示等待任一子进程;若pid>0
表示等待其进程ID与pid相等的子进程;若pid=0
表示等待进程组ID与当前进程相同的任何子进程;若pid<-1
表示等待其组ID等于pid的绝对值的任一子进程(适用于子进程在其它组的情况)。
若我们不关心子进程的返回状态以及返回值,则可以将wstatus
传为NULL
。wstatus
的操作内容比较多,下面介绍两个常用的。
操作 | 作用 |
---|---|
WIFEXITED(wstatus) | 为非0表示程序正常结束 |
WEXITSTATUS(wstatus) | 获取进程的退出状态也就是返回值 |
WIFSIGNALED(wstatus) | 为非0表示程序异常终止 |
WTERMSIG(wstatus) | 获取进程终止的信号编号 |
下面给出一个这两个函数的使用案例。使用wait
回收子程序资源的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main()
{pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid > 0){// 父进程printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());int wstatus;pid_t wpid = wait(&wstatus);if(wpid < 0){printf("There are no child processes to reclaim\n");}else {if(WIFEXITED(wstatus)){// 正常退出printf("The process terminated normally, return = [%d]\n", WEXITSTATUS(wstatus));printf("Reclaim to child process wpid = [%d]\n", wpid);}else if(WTERMSIG(wstatus)){// 被信号杀死printf("The process is killed by signal, signal is [%d]\n", WTERMSIG(wstatus));printf("Reclaim to child process wpid = [%d]\n", wpid);}}}else{// 子进程printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());sleep(15);return 100;}return 0;
}
使用waitpid
的示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/wait.h>int main()
{srand(time(NULL));printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());pid_t f_pid = getpid(); // 父亲的pidfor(int i = 0; i < 4; i ++){sleep(rand() % 2);pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());break;}}if(getpid() == f_pid){// 父进程pid_t wpid = 0;int wstatus = 0;// 等待任意一个子进程,非阻塞while((wpid = waitpid(-1, &wstatus, WNOHANG)) != -1){// 有进程死亡if(wpid > 0){if(WIFEXITED(wstatus)){// 正常死亡printf("The process [%d] terminated normally, return = [%d]\n", wpid, WEXITSTATUS(wstatus));}else if(WIFSIGNALED(wstatus)){// 信号杀死printf("The process [%d] is killed by signal [%d]\n", wpid, WTERMSIG(wstatus));}}}}else{// 子进程int s_time = rand() % 10 + 10;int r_number = rand() % 10;printf("The process [%d], the father is %d, return [%d], sleep time [%d]s\n", getpid(), getppid(), r_number, s_time);sleep(s_time);return r_number;}return 0;
}