进程
- 目录
- 1. 进程的创建
- 主要功能
- 接口解析
- 示例代码
- 程序执行结果
- 结果说明
- 2. 进程的回收
- 主要功能
- 接口解析
- 示例代码
- 执行结果
- 代码解析
- `waitpid`函数
- 与`wait()`的区别
- 3. 加载并执行指定程序
- 主要功能
- 接口解析
- 注意事项
- 示例代码
- 程序运行结果
- 程序解析
- 「课堂练习」
- 练习 2
- 练习 3
目录
1. 进程的创建
#include <sys/types.h>
#include <unistd.h>pid_t fork(void);
主要功能
将当前的进程复制一份,然后这两个进程同时从本函数的下一语句开始执行。
接口解析
所有的代码、变量都会复制成两份。该函数会返回两次,一次返回父进程,值是子进程的PID,一次返回子进程,值固定为0。父子进程是并发执行的,没有先后次序,若要控制次序,要依赖于信号量、互斥锁、条件量等其他条件。
示例代码
#include <stdio.h>
#include <unistd.h> int main()
{printf("[]fork之前\n");pid_t pid = fork();// 以上函数执行成功后// 父子进程都将从下面的语句开始执行,不分先后// 以下语句会被执行两遍// 在父进程中,pid将是子进程的PID// 在子进程中,pid将是0printf("[%d]: pid=%d\n", getpid(), pid);// 让父进程等待子进程输出if (pid > 0) {wait(NULL);}
}
程序执行结果
shaseng@ubuntu:$ ./a.out
[5140]: fork之前
[5141]: pid=0
[5140]: pid=5141
shaseng@ubuntu:$
结果说明
在执行fork()函数之前,printf("[]fork之前\n");代码只执行一遍,并且是父进程[5140]在执行它。在执行fork()函数之后,进程分裂成两个,因此printf("[%d]: pid=%d\n", getpid(), pid);代码被执行了两遍。函数getpid()的功能是获取自身进程的PID,在程序该行,父进程和子进程分别输出了自己的PID,一个是5140,一个是5141。在5140那边,输出的pid是5141,于是我们得知5140必然是父进程,因为只有父进程才能获取一个大于零的子进程的PID。在5141那边,输出的pid是0,于是我们得知5141必然是子进程,因为只有子进程才会从fork()的返回值中获取一个0。通过在父进程中调用wait(NULL);,让父进程等待子进程执行完毕,避免了bash的命令行信息穿插到父子进程中间。
2. 进程的回收
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *wstatus);
主要功能
阻塞当前进程,等待其子进程退出并回收其系统资源。
接口解析
如果当前进程没有子进程,则该函数立即返回。如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h> int main()
{if(fork() == 0){printf("[%d]: 我将在3秒后正常退出,退出值是88\n", getpid());for(int i=3; i>=0; i--){fprintf(stderr, " ======= %d =======%c", i, i==0?'\n':'\r');sleep(1);}exit(88);}else{printf("[%d]: 我正在试图回收子进程的资源...\n", getpid());int status;wait(&status);if(WIFEXITED(status)){printf("[%d]: 子进程正常退出了,其退出值是:%d\n", getpid(), WEXITSTATUS(status));}}
}
执行结果
shaseng@ubuntu:$ ./a.out
[3611]: 我正在试图回收子进程的资源...
[3612]: 我将在3秒后正常退出,退出值是88======= 0 =======
[3611]: 子进程正常退出了,其退出值是:88
shaseng@ubuntu:$
代码解析
status用来存放子进程的退出状态,status包含了子进程退出的诸多信息,而不仅仅是退出值,因此父进程如果要获取这些信息,需要用以下宏对status进行解析:
| 宏 | 功能 |
|---|---|
WIFEXITED(status) | 判断子进程是否正常退出 |
WEXITSTATUS(status) | 获取正常退出的子进程的退出值 |
WIFSIGNALED(status) | 判断子进程是否被信号杀死 |
WTERMSIG(status) | 获取杀死子进程的信号的值 |
waitpid函数
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *wstatus, int options);
与wait()的区别
可以通过参数pid用来指定想要回收的子进程,可以通过options来指定非阻塞等待。pid和options这两个参数的取值和作用详见下表:
pid | 作用 | options | 作用 |
|---|---|---|---|
< -1 | 等待组ID等于pid绝对值的进程组中的任意一个子进程 | 0 | 阻塞等待子进程的退出 |
-1 | 等待任意一个子进程 | WNOHANG | 若没有僵尸子进程,则函数立即返回 |
0 | 等待本进程所在的进程组中的任意一个子进程 | WUNTRACED | 当子进程暂停时函数返回 |
> 0 | 等待指定pid的子进程 | WCONTINUED | 当子进程收到信号SIGCONT继续运行时函数返回 |
注意:options的取值,可以是0,也可以是上表中各个不同的宏的位或运算取值。
3. 加载并执行指定程序
#include <unistd.h>extern char **environ;int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...);int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
主要功能
给进程加载指定的程序,如果成功,进程的整个内存空间都被覆盖。
接口解析
执行指定程序之后,会自动获取原来的进程的环境变量。各个后缀字母的含义:
l: list 以列表的方式来组织指定程序的参数v: vector 矢量、数组,以数组的方式来组织指定程序的参数e: environment 环境变量,执行指定程序前顺便设置环境变量p: 专指PATH环境变量,这意味着执行程序时可自动搜索环境变量PATH的路径
这组函数只是改变了进程的内存空间里面的代码和数据,但并未改变本进程的其他属性。
注意事项
以execl(const char *path, const char *arg, ...)为例,参数path是需要加载的指定程序,而arg则是该程序运行时的命令行参数,值得注意的是,命令行参数包括程序名本身,并且全部是字符串。例如:
shaseng@ubuntu:$ ./a.out 123 abc
上述命令用execl来指定则是:
execl("./a.out", "./a.out", "123", "abc", NULL);
这其中:第一个./a.out是程序本身,第二个./a.out是第一个参数。参数列表以NULL结尾。
示例代码
// child.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h> int main(int argc, char **argv)
{// 倒数 n 秒for(int i=atoi(argv[1]); i>0; i--){printf("%d\n", i);sleep(1);}// 程序退出,返回 nexit(atoi(argv[1]));
}// main.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> int main()
{// 子进程if(fork() == 0){printf("加载新程序之前的代码\n");// 加载新程序,并传递参数3if (execl("./child", "./child", "3", NULL) == -1) {perror("execl failed");exit(EXIT_FAILURE);}printf("加载新程序之后的代码\n");}// 父进程else{// 等待子进程的退出int status;int ret = waitpid(-1, &status, 0);if(ret > 0){if(WIFEXITED(status))printf("[%d]: 子进程[%d]的退出值是:%d\n",getpid(), ret, WEXITSTATUS(status));}else{printf("暂无僵尸子进程\n");}}
}
程序运行结果
shaseng@ubuntu:$ gcc child.c -o child
shaseng@ubuntu:$ gcc main.c -o main
shaseng@ubuntu:$ ./main
加载新程序之前的代码
3
2
1
[5634]: 子进程[5635]的退出值是:3
shaseng@ubuntu:$
程序解析
子进程中加载新程序之后的代码无法运行,因为已经被覆盖了。waitpid()中指定了options的值为0,意味着阻塞等待子进程,效果跟直接调用wait()相当。
「课堂练习」
练习 2
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> int main()
{printf("[]fork之前\n");pid_t pid = fork();if (pid < 0) {perror("fork failed");return 1;} else if (pid == 0) {// 子进程printf("[%d]: pid=%d\n", getpid(), pid);} else {// 父进程int status;wait(&status);printf("[%d]: pid=%d\n", getpid(), pid);}return 0;
}
练习 3
// child.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h> int main(int argc, char **argv)
{// 倒数 n 秒for(int i=atoi(argv[1]); i>0; i--){printf("%d\n", i);sleep(1);}// 程序退出,返回 nexit(atoi(argv[1]));
}// main.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> int main()
{pid_t pid = fork();if (pid < 0) {perror("fork failed");return 1;} else if (pid == 0) {// 子进程printf("加载新程序之前的代码\n");if (execl("./nonexistent_child", "./nonexistent_child", "3", NULL) == -1) {perror("execl failed");exit(EXIT_FAILURE);}printf("加载新程序之后的代码\n");} else {// 父进程// 模拟父进程先退出printf("[%d]: 父进程即将退出\n", getpid());return 0;}return 0;
}
在这个练习中,子进程尝试加载一个不存在的程序,会导致execl失败并输出错误信息。父进程先于子进程退出,子进程会成为孤儿进程,被init进程(systemd)收养。
