linux系统编程学习_(2)进程控制-- fork函数、exec函数族、回收子进程–孤儿进程僵尸进程、wait函数
进程控制
fork()函数
创建一个子进程。
pid_t fork(void);
pid_t类型是表示进程ID,但为了表示-1,它是有符号整型(0并不是有效进程ID,init最小,为1)
失败返回: -1
成功返回:
- 父进程返回子进程的ID(非负)
- 子进程返回0
getpid()函数、getppid()函数、getuid()函数、getgid()函数
获取当前进程ID pid_t getpid(void);
获取当前进程的父进程的ID:pid_t getppid(void);
获取当前进程实际用户ID:uid_t getuid(void);
获取当前进程有效用户ID:uid_t geteuid(void);
获取当前进程使用用户组ID:gid_t getgid(void);
获取当前进程有效用户组ID:git_t getegid(void);
进程共享
父子进程之间在fork后,有哪些相同与不同?
父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处:进程ID、 fork返回值、父进程ID、进程运行时间、闹钟(定时器)、未决信号集
似乎是子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同,真的每fork一个子进程都要将父进程0-3G地址空间完全拷贝一份然后映射至物理内存吗?
显然不是,父子进程间遵循读时共享写时复制的原则,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。父子之间的全局变量是独享的,特别的,fork之后,父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法。
gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程还是子进程,默认跟踪父进程。
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程
set follow-fork-mode parent 设置跟踪父进程
一定要在fork函数调用之前设置才有效。
示例
// fork.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int var = 34;
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1 ) {
perror("fork");
exit(1);
} else if (pid > 0) {
sleep(2);
var = 55;
printf("I'm parent, pid = %d, ppid = %d, var = %d\n", getpid(), getppid(), var); // var=55;
} else if (pid == 0) {
var = 100;
printf("I'm child, pid = %d, ppid=%d, var = %d\n", getpid(), getppid(), var); // var=100;
}
printf("var = %d\n", var); // 父进程var=55,子进程var=100;
return 0;
}
// loop_fork.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int n = 5, i; //默认创建5个子进程
if(argc == 2){
n = atoi(argv[1]);
}
for(i = 0; i < n; i++) //出口1,父进程专用出口
if(fork() == 0)
break; //出口2,子进程出口,i不自增
if(n == i){
sleep(n);
printf("I am parent, pid = %d\n", getpid());
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n", i+1, getpid());
}
return 0;
}
exec函数族
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以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, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
execl()函数加载一个进程,通过 路径+程序名 来加载,成功无返回,失败-1。
对比execlp,如加载"ls"命令带有-l,-F参数:
execlp("ls","ls","-l","-F",MULL); // 使用程序名在PATH中搜索
execl("/bin/ls", "ls", "-l", "-F", NULL); // 使用参数1给出的绝对路径搜索
示例
// execl.c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid > 0) {
execl("/home/itcast/0105_Linux/process_test/exec/output", "output", NULL);
} else if (pid == 0) {
printf("i'm parent pid = %d\n", getpid());
}
return 0;
}
// execl2.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("========================\n");
char *argvv[] = {"ls", "-l", "-F", "R", "-a", NULL};
pid_t pid = fork();
if (pid == 0) {
execl("/bin/ls", "ls", "-l", "-F", "-a", NULL);
execv("/bin/ls", argvv);
perror("execlp");
exit(1);
} else if (pid > 0) {
sleep(1);
printf("parent\n");
}
return 0;
}
execlp()函数:加载一个进程,借助PATH环境变量,成功无返回,失败是-1。
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
示例
// exec_ps.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd;
fd = open("ps.out", O_WRONLY|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open ps.out error");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "ax", NULL);
//close(fd);
return 0;
}
execvp()函数: 加载一个进程,使用自定义环境变量env
变参形式: argv[] 变参终止条件 1. NULL结尾;2. 固参指定。
execvp与execlp参数形式不同,原理一致。
示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid ;
pid = fork();
if (pid > 0) {
sleep(3);
} else if (pid == 0) {
//ls -l
char *argv[] = {"ls", "-l", "-a", NULL};
execvp("ls", argv);
perror("execlp error");
exit(1);
}
return 0;
}
exec函数族一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
l (list) 命令行参数列表
p (path) 搜素file时使用path变量
v (vector) 使用命令行参数数组
e(environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
回收子进程
孤儿进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,则子进程的父进程变成为init进程,称为init进程领养孤儿(orphan)进程。
// orphan.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid == 0) {
while (1) {
printf("I am child, my parent pid = %d\n", getppid());
sleep(1);
}
} else if (pid > 0) {
printf("I am parent, my pid is = %d\n", getpid());
sleep(9);
printf("------------parent going to die------------\n");
} else {
perror("fork");
return 1;
}
return 0;
}
僵尸进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
// zoom.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
pid = fork();
if (pid == 0) {
printf("I am child, my parent= %d, going to sleep 10s\n", getppid());
sleep(10);
printf("-------------child die--------------\n");
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
wait()函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态(退出原因)。
pid_t wait(int *status); // 成功:清理掉的子进程ID; 失败:-1(没有子进程)
当进程终止时,操作系统的隐式 回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
- WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)- WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。- WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
示例
// wait1.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(7); //困了...
} else {
lable:
wpid = wait(NULL); //死等!!!
if(wpid == -1){
perror("wait error");
goto lable;
}
printf("I'm parent, I catched child process,"
"pid = %d\n", wpid);
}
return 0;
}
// wait2.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
#if 1
execl("./abnor", "abnor", NULL);
perror("execl error");
exit(1);
#endif
sleep(1);
exit(10);
} else {
//wpid = wait(NULL); //传出参数
wpid = wait(&status); //传出参数
if(WIFEXITED(status)){ //正常退出
printf("I'm parent, The child process "
"%d exit normally\n", wpid);
printf("return value:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { //异常退出
printf("The child process exit abnormally, "
"killed by signal %d\n", WTERMSIG(status));
//获取信号编号
} else {
printf("other...\n");
}
}
return 0;
}
waitpid()函数
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options); // 成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid:
>0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程
返回0:参3为WNOHANG,且子进程正在运行。 // 非阻塞状态,则用dowhile设为轮询 即参3如此设置后,返回的pid==0,表示子进程正在运行
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
示例
// waitpid.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, pid2, wpid;
int flg = 0;
pid = fork();
pid2 = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(5);
exit(4);
} else { //parent
do {
wpid = waitpid(pid, NULL, WNOHANG);
//wpid = wait(NULL);
printf("---wpid = %d--------%d\n", wpid, flg++);
if(wpid == 0){
printf("NO child exited\n");
sleep(1);
}
} while (wpid == 0); //子进程不可回收
if(wpid == pid){ //回收了指定子进程
printf("I'm parent, I catched child process,"
"pid = %d\n", wpid);
} else {
printf("other...\n");
}
}
return 0;
}
// waitpid2.c
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
int i;
for (i = 3; i > 0; i--) {
printf("This is the child\n");
sleep(1);
}
exit(34);
} else {
int stat_val;
waitpid(pid, &stat_val, 0); //阻塞
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
return 0;
}
// waitpid3.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int n = 5, i;
pid_t p, q;
if(argc == 2){
n = atoi(argv[1]);
}
q = getpid();
for(i = 0; i < n; i++) {
p = fork();
if(p == 0) {
break;
}
}
if(n == i){ // parent
sleep(n);
printf("I am parent, pid = %d\n", getpid());
for (i = 0; i < n; i++) {
p = waitpid(0, NULL, WNOHANG);
printf("wait pid = %d\n", p);
}
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n",
i+1, getpid());
}
return 0;
}
参考
黑马程序员服务器开发课程资料