进程
目录
1. 进程的创建
c
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
主要功能
将当前的进程复制一份,然后这两个进程同时从本函数的下一语句开始执行。
接口解析
所有的代码、变量都会复制成两份。该函数会返回两次,一次返回父进程,值是子进程的PID,一次返回子进程,值固定为0。父子进程是并发执行的,没有先后次序,若要控制次序,要依赖于信号量、互斥锁、条件量等其他条件。
示例代码
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("[]fork之前\n");
pid_t pid = fork();
// 以上函数执行成功后
// 父子进程都将从下面的语句开始执行,不分先后
// 以下语句会被执行两遍
// 在父进程中,pid将是子进程的PID
// 在子进程中,pid将是0
printf("[%d]: pid=%d\n", getpid(), pid);
// 让父进程等待子进程输出
if (pid > 0) {
wait(NULL);
}
}
程序执行结果
plaintext
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. 进程的回收
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
主要功能
阻塞当前进程,等待其子进程退出并回收其系统资源。
接口解析
如果当前进程没有子进程,则该函数立即返回。如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。子进程的退出状态(包括退出值、终止信号等)将被放入wstatus
所指示的内存中,若wstatus
指针为NULL
,则代表当前进程放弃其子进程的退出状态。
示例代码
c
#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));
}
}
}
执行结果
plaintext
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
函数
c
#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. 加载并执行指定程序
c
#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
则是该程序运行时的命令行参数,值得注意的是,命令行参数包括程序名本身,并且全部是字符串。例如:
plaintext
shaseng@ubuntu:$ ./a.out 123 abc
上述命令用execl
来指定则是:
c
execl("./a.out", "./a.out", "123", "abc", NULL);
这其中:第一个./a.out
是程序本身,第二个./a.out
是第一个参数。参数列表以NULL
结尾。
示例代码
c
// 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);
}
// 程序退出,返回 n
exit(atoi(argv[1]));
}
// main.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
// 子进程
if(fork() == 0)
{
printf("加载新程序之前的代码\n");
// 加载新程序,并传递参数3
if (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");
}
}
}
程序运行结果
plaintext
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
c
#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
c
// 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);
}
// 程序退出,返回 n
exit(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
)收养。