进程等待
前面我们了解了如果父进程没有回收子进程, 那么当子进程接收后, 就会一直处于僵尸状态, 导致内存泄漏, 那么我们如何让父进程来回收子进程的资源.
waitpid
我们可以通过 Linux 提供的系统调用函数 wait 系列函数来等待子进程死亡, 并回收资源.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait
函数用于等待任何一个子进程结束, 并回收其资源.
status
:指向整数的指针, 用于存储子进程的退出状态. 如果不需要这个信息, 可以传递NULL.
- 成功时返回被等待的子进程的PID, 失败时返回 -1, 并设置 errno.
waitpid
函数允许父进程等待特定的子进程结束
pid
:子进程的PID. 如果为 -1, 则等待任何一个子进程。status
:同wait
函数.options
:等待选项, 常用的有 WNOHANG (非阻塞等待).- 成功时返回被等待的子进程的PID. 失败时返回
-1
,并设置errno
。
一般来说, 用 waitpid 多一点, 因为 waitpid 提供的更为细致的操作.
int main()
{ pid_t id = fork(); if(id<0) { perror("fork"); exit(1); } if(id==0)//子进程代码 { int count = 5; while(count) { printf("[%d]我是子进程,我的pid是: %d\n",count,getpid()); sleep(1); count--; } exit(0);//子进程执行完代码后退出, exit 会直接终止本进程 } //父进程代码 waitpid(id,NULL,0); printf("等待子进程成功!\n"); return 0;
}
可以观察到, 在等待子进程结束之前, 父进程卡在了 waitpid(), 直到子进程都被等待成功, 父进程才会继续向后执行.
status 参数
在 wait 和 waitpid 函数中都存在一个 status 的参数.
在 status 中存储着子进程的退出码和退出信号.
如果父进程想要了解子进程的退出信息, 可以通过 status 来了解.
status 是一个 int 类型的变量, 一共有 32 个bit, 我们主要看它的低 16 位bit.
那么如何获取退出状态 (退出码) 和 信号
退出状态 (退出码):
(status >> 8) & 0xFF
退出信号:
status & 0x7F
int main()
{ pid_t id = fork(); if(id<0) { perror("fork"); exit(1); } if(id==0)//子进程代码 { int count = 5; while(count) { printf("[%d]我是子进程,我的pid是: %d\n",count,getpid()); sleep(1); count--; } exit(55);//子进程执行完代码后退出 } //父进程代码 int status = 0; waitpid(id,&status,0); printf("等待子进程成功!\n"); printf("进程退出码: %d,进程退出信号: %d\n",(status >> 8) & 0xFF,status & 0x7F); return 0;
}
当子进程在运行时, 使用 kill -9 617714命令, 来终止子进程, 那么也就能观察到, 退出信号为 9.
option
在前面的参数解释中说到, 这是一个等待选项.
父进程可以选择一直阻塞下去, 直到等到子进程的死亡,
或者当子进程还没死亡时, 父进程去执行其他的代码. 等一会再来查看子进程是否死亡.
waitpid(pid,&status,WNOHANG); // 非阻塞等待
waitpid(pid,&status,0); // 阻塞等待
int main()
{pid_t id = fork();if(id<0){perror("fork");exit(1);}if(id==0)//子进程代码{int count = 5;while(count){printf("[%d]我是子进程,我的pid是: %d\n",count,getpid());sleep(1);count--;}exit(55);//子进程执行完代码后退出}//父进程代码while(1)//循环访问子进程退出情况{int wait = waitpid(id,NULL,WNOHANG);if(wait>0)//子进程退出成功{printf("子进程退出成功,子进程pid: %d\n",wait);break;}else if(wait==0)//子进程还没退出,父进程干自己的事情{//此处简单模拟父进程干的事情printf("我是父进程\n");}else //等待子进程退出失败{perror("waitpid");exit(1);}sleep(1);}return 0;
}
执行上面的代码就能观察到, 在子进程结束前, 父进程还在向频幕上打印文字.
进程替换
创建子进程是为了完成一些工作, 但是子进程的代码和父进程是一样的,
有可能子进程并不需要执行父进程的代码, 而是执行一些其他代码.
这种场景下, 就可以使用进程替换.
我们为什么不直接将子进程要执行的代码写在父进程中呢, 还要去使用进程替换?
1. 如果所有的代码都放在父进程中, 那么父进程的代码会有多么的庞大,
这会提高代码编写和维护的成本.
2. 进程所执行的一定是我们的C/C++程序吗? 进程也可以执行其他的非 C/C++ 程序,
那对于那些非 C/C++ 程序 (java程序), 我们无法将他们和我们所写的 C/C++ 代码整合在一起, java 和 C/C++ 的运行环境都不同.
exec 系列函数
如果想要创建出来的子进程执行全新的程序, 可以使用 exec 系列函数进行程序替换.
一共有6个函数, 其中主要分为两类
1. execl 系列
2. execv 系列
execl
int main()
{ printf("进行程序替换了\n"); int n = execl("/usr/bin/ls","ls","-a","-l",NULL); if(n==-1) { perror("execl"); } printf("程序替换完毕!\n"); return 0;
}
execl参数: 第一个是要执行程序的路径 (/usr/bin/ls),
第二个参数是要执行的程序的名称 (ls),后面的参数到 NULL 之前, 都是要替换的程序参数 (-a, -l, 都是 ls 程序的参数).
execl 中 l, 表示如何将参数传递要替换的程序. l 表示通过一个列表的方式,
即向上面的 "-l", "-a"..., 一个列表的形式.
execlp 和 execle 两个函数则分别多了 p 和 e.
p 则代表要执行的程序可以从环境变量 PATH 中找到, 所以不用写执行程序的路径.
e 则表示, 可以传入用户自己定义的环境变量 (_env[]) 给程序使用.
int main()
{ printf("我要进行程序替换了...\n"); int n = execlp("ls","-l",NULL); if(n==-1) { perror("execl"); } printf("程序替换完毕!\n"); return 0;
} int main()
{ const char* _env[]={"MY_ENV=666",NULL}; printf("我要进行程序替换了...\n"); int n = execle("/usr/bin/ls","ls","-l",NULL,_env);//自己定义一个环境变量MY_ENV=666传递给要去执行的程序 if(n==-1) { perror("execl"); } printf("程序替换完毕!\n"); return 0;
}
execv
上面的 execl 中的 l, 代表传参使用列表的形式.
那么 v 很容易就想到了是vector.
所以 execv 函数在给替换的程序传参时, 是通过一个 vector 来传参的.
int main() { char* const set[]={"ls","-a","-l",NULL}; printf("我要进行程序替换了...\n"); int n = execv("/usr/bin/ls",set); if(n==-1) { perror("execl"); } printf("程序替换完毕!\n"); return 0; }
那么剩下的 execvp 和 execvpe 和之前的 execl 系列中的一样.
p 代表在环境变量 PATH 中查找, e 可以传入自己定义的环境变量.
- l (list): 传参的方式为使用列表来传递
- v (vector): 使用数组来传递参数
- p (path): 会在环境变量 PATH 中查找程序
- e (env): 可以传递自己定义的环境变量