12.进程控制
12.进程控制
1.学习目标
-
了解进程相关的概念
-
掌握fork/getpid/getppid函数的使用
-
熟练掌握ps/kill命令的使用
-
熟练掌握execl/execlp函数的使用
-
说出什么是孤儿进程什么是僵尸进程
-
熟练掌握wait函数的使用
-
熟练掌握waitpid函数的使用
2.进程相关概念
2.1 程序和进程
-
程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间,是一个静态的概念。
-
进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念
-
程序->剧本(纸)
-
进程->戏(舞台、演员、灯光、道具...)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)
2.2 并行和并发
- 并发,在一个时间段内,是在同一个cpu上,同时运行多个程序。
如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。
- 并行性指两个或两个以上的程序在同一时刻发生(需要有多颗)。
2.3 PCB-进程控制块
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-4.4.0-96/include/linux/sched.h文件的1390行处可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
-
进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
-
进程的状态,有就绪、运行、挂起、停止等状态。
-
进程切换时需要保存和恢复的一些CPU寄存器。
-
描述虚拟地址空间的信息。
-
描述控制终端的信息。
-
当前工作目录(Current Working Directory)。
█getcwd --pwd
-
umask掩码。
-
文件描述符表,包含很多指向file结构体的指针。
-
和信号相关的信息。
-
用户id和组id。
-
会话(Session)和进程组。
-
进程可以使用的资源上限(Resource Limit)。
█ulimit -a
cmt@cmt - VMware - Virtual - Platform:~$ sleep 100
cmt@cmt - VMware - Virtual - Platform:~$ ps - ef | grep sleep
cmt 21069 8308 0 14 : 13 pts / 1 00 : 00 : 00 sleep 100
cmt 21078 21070 0 14 : 14 pts / 2 00 : 00 : 00 grep --color = auto sleep
cmt@cmt - VMware - Virtual - Platform:~$ ps - ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3454 8308 8308 8308 pts/1 21069 Ss 1000 0:00 bash
8308 21069 21069 8308 pts / 1 21069 S + 1000 0:00 sleep 100
3454 21070 21070 21070 pts / 2 21079 Ss 1000 0:00 bash
21070 21079 21079 21070 pts / 2 21079 R + 1000 0:00 ps - ajx
cmt@cmt - VMware - Virtual - Platform:~$ id
uid = 1000(cmt) gid = 1000(cmt) 组 = 1000(cmt), 4(adm), 24(cdrom), 27(sudo), 30(dip), 46(plugdev), 100(users), 118(lpadmin)
PPID是父进程ID,PID是自己的ID,PGID是组ID,SID是会话ID
cmt@cmt-VMware-Virtual-Platform:~$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15192
max locked memory (kbytes, -l) 495516
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15192
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
2.4 进程状态(面试考)
- 进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
3.创建进程
3.1 fork函数
-
函数作用:创建子进程
-
原型: pid_t fork(void);
函数参数:无
返回值:调用成功:父进程返回子进程的PID,子进程返回0;
调用失败:返回-1,设置errno值。
● fork函数代码片段实例
- 调用fork函数的内核实现原理:
● fork函数总结
►fork函数的返回值?
父进程返回子进程的PID,是一个大于0数;
子进程返回0;
特别需要注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。
►子进程创建成功后,代码的执行位置?
父进程执行到什么位置,子进程就从哪里执行
►如何区分父子进程
通过fork函数的返回值
►父子进程的执行顺序
不一定,哪个进程先抢到CPU,哪个进程就先执行
我们先来看个代码,判断一下这个代码的输出结果会是什么样的,先不要去看运行结果,判断好后再去看看是否和你的预期结果一致。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
pid_t pid;
pid = fork();
printf("xxxxxxxxxx\n");
while (1)
{
sleep(1);
}
return 0;
}
输出:
运行结果:
xxxxxxxxxx
xxxxxxxxxx
是不是和你预想的结果不太一样呢?为什么会是输出两遍呢?这是什么原理呢?
抱着这样的问题,让我们来研究一下fork函数的奥秘吧。
fork函数
功能:创建一个与原来进程几乎完全相同的进程
这也就代表着,父进程可通过调用该函数创建一个子进程,两个进程可以做完全相同的事
返回值:pid_t类型的变量,也就是进程id类型的变量
这里有个非常让人惊讶的地方,fork函数的返回值是2个!!!
想想自己学了那么久的编程,好像没有返回值是两个的函数啊。别慌,接着往下看
我们来对父进程通过fork函数创建子进程的过程做个具体的说明,上图!
在上述这个图中,当调用fork函数时,操作系统会从用户态切换回内核态来进行进程的创建,会调用fork函数中的_CREATE函数和_CLONE函数。
首先调用_CREATE函数,子进程进行虚拟地址申请,在子进程的内核空间中进行不完全拷贝,为什么是不完全拷贝呢?就像父亲和儿子的关系一样,你可以和你爸爸的民族,籍贯所在地一样,但你不能和你爸的年龄,身份证号都一样吧。PCB作为每个进程的唯一标识符,就像每个人的身份证一样,是不可能完全一样的,所以这个地方时不完全拷贝,如pid就需要自己生成。这个地方的子进程是新生态。
之后调用_CLONE函数,向父进程拷贝必要资源,子进程的用户空间进行完全拷贝,子进程继承所有父进程资源,如临时数据堆栈拷贝,代码完全拷贝。
这个时候就有善于思考的同学会发现,并提出以下问题:
诶诶诶,你这父进程创建一个子进程,你这子进程把你的代码完全拷贝走了。
-
那子进程不是把fork函数也拷贝走了吗?
-
那子进程不也可以通过fork函数创建孙线程了吗?
-
那你这不是子又生孙,孙又生子吗?
-
那你这不无限创造进程了吗?
-
那为什么上面的代码的运行结果只有两个输出?
-
考虑的非常好啊,这也是我们下面要讲的问题
讲解完父进程如何通过fork函数创建子进程,接下来我们就要讲解父子进程如何执行fork函数
上图!
其实大体来说,我们可以将fork函数分为三步
1.调用_CREATE函数,也就是进程创建部分
2.调用_CLONE函数,也就是资源拷贝部分
3.进程创建成功,return 0; 失败,return -1:
前2步也就是父进程通过fork函数创建子进程的步骤,在执行完_CLONE函数后,fork函数会有第一次返回,子进程的pid会返回给父进程。要注意的是,在第3步中,fork函数不是由父进程来执行,而是由子进程来执行,当父进程执行完_CLONE函数后,子进程会执行fork函数的剩余部分,执行最后这个语句,fork函数就会有第二次返回,如果成功就返回0,失败就返回-1。
我们就可以总结得出,父子进程都执行fork函数,但执行不同的代码段,获取不同的返回值。所以fork函数的返回值情况如下:
父进程调用fork,返回子线程pid(>0)
子进程调用fork,子进程返回0,调用失败的话就返回-1
这也就说明了fork函数的返回值是2个
可以通过下面的代码来验证该过程
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
//Parent Start
pid_t pid;
pid = fork();
if (pid > 0)
{
printf("parent running\n");
while (1)
{
sleep(1);
}
}
else if (pid == 0)
{
//Child Start
printf("Child Running\n");
while (1)
{
sleep(1);
}
//Child End
}
else
{
perror("fork call failed\n");
}
while (1)
{
sleep(1);
}
return 0;
}
//Parent End
输出:
运行结果:
parent running
Child Running
该程序首先包含了四个头文件,这些头文件提供了程序中使用的各种函数和功能。关键函数是fork()
,它来自于unistd.h
,用于在UNIX和Linux系统上创建新进程。
接下来,我们逐步解析程序的运行流程:
- 主进程开始:当程序开始执行时,它是在一个称为“主进程”或“父进程”中运行的。
- 创建子进程:程序使用
fork()
函数创建一个新进程,这个新进程称为“子进程”。这个子进程是父进程的副本,继承了父进程的数据和代码。 - 父进程执行:如果
fork()
调用成功,对于父进程,fork()
返回子进程的进程ID,它肯定是一个大于0的值。于是,程序进入if (pid > 0)
分支,打印parent running
,然后进入一个无限循环,在该循环中,每秒都会休眠一次。 - 子进程执行:对于子进程,
fork()
返回0。所以,程序进入else if (pid == 0)
分支,打印Child Running
,然后进入其自己的无限循环,并每秒休眠一次。 - fork失败:如果
fork()
调用失败,它返回-1。在这种情况下,程序进入else
分支,并使用perror
打印一个错误消息。 - 程序结束:无论
fork()
的结果如何,程序都会进入最后的while
循环,并开始另一个无限循环。但实际上,由于之前的逻辑(父进程、子进程、或fork
失败的错误处理),这个循环是不可达的,也就是说,程序永远不会执行到这里。
总结:
- 父进程会打印
parent running
并进入一个无限循环。 - 子进程会打印
Child Running
并进入一个无限循环。 - 如果
fork
失败,程序会打印一个错误消息。
如果你运行这个程序,你会发现父进程和子进程都在无限循环中运行,且都可以在进程列表中看到(例如,使用ps
命令在UNIX/Linux系统上)。
为什么 最后的while (1)不会运行?
在上述代码中,fork()
调用后,根据其返回值,程序流会进入其对应的条件分支:父进程、子进程或fork
失败。
-
父进程:对于父进程,
fork()
返回子进程的PID(一个大于0的值),所以程序会进入if (pid > 0)
分支,并在那里遇到一个无限循环(while(1)
)。由于这是一个无限循环,程序将永远停留在这里,不会退出该循环。 -
子进程:对于子进程,
fork()
返回0。因此,程序会进入else if (pid == 0)
分支,同样地,它也会在这里遇到一个无限循环。子进程将永远停留在这个循环中。 -
fork失败:如果
fork()
失败,它返回-1。程序将进入else
分支,并打印一个错误消息。然后,它会到达函数的末尾并退出,而不会执行最后的while(1)
循环。
因此,不论程序执行路径如何,它都会在最后的while(1)
循环之前进入一个其他的无限循环或者完全结束运行,这就是为什么最后的while(1)
循环不会被执行的原因。
另外和大家说一个小知识点
整个Linux操作系统都是由父子进程结构构成
每个进程都有创建者,也就是父进程,但是有一个进程例外,也就是init进程
init进程(0 or 1),init进程是系统启动初始化后的第一个进程
例子:
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d]\n", getpid());
sleep(1);
}
else if(pid == 0)//子进程
{
printf("child: pid == [%d]\n", getpid());
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make fork
cc fork.c -o fork
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./fork
father: pid==[22119]
child: pid == [22120]
先创建的PID小,后创建的大。
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork, pid:[%d]\n", getpid());
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d]\n", getpid());
sleep(1);
}
else if(pid == 0)//子进程
{
printf("child: pid == [%d]\n", getpid());
}
printf("after fork, pid:[%d]\n", getpid());
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ls
fork fork.c
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make fork
cc fork.c -o fork
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./fork
before fork, pid:[22217]
father: pid==[22217]
child: pid == [22218]
after fork, pid:[22218]
after fork, pid:[22217]
if...else之外的语句父子进程都有。
父进程返回值大于0,子进程返回0(创建子进程成功)。父子谁先执行用户无法控制。sleep(1)让父进程后退出(子进程也不见得先执行,概率高一点而已)。
如果父进程先退出,子进程后退出,子进程PID会变化吗?
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork, pid:[%d]\n", getpid());
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
sleep(1);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
}
printf("after fork, pid:[%d]\n", getpid());
return 0;
}
2815是当前shell
注释sleep(1)
在Linux系统中,进程号1(PID 1)通常是init
进程,或者在现代系统上是systemd
进程。这个进程在系统启动时由内核启动,并且是所有其他用户空间进程的祖先。很多进程的父进程,很多进程是被它拉起来的。
./fork是被当前shell拉起来的
第一个fork中,父进程后退出,第二个fork中子进程后退出
子进程被1号进程领养,进程号1(PID 1)通常是init
进程
有可能在这阻塞了
敲回车立刻退出,说明没有阻塞。
父进程结束,子进程后结束。子进程还在占用,父进程先回到当前shell
回到shell
PCB里有文件描述符表存放打开的文件的信息,父子文件共享标准输出
3.2 ps命令和kill命令
-
ps aux | grep "xxx"
-
ps ajx | grep "xxx"
▶ -a:(all)当前系统所有用户的进程
▶-u:查看进程所有者及其他一些信息
▶-x:显示没有控制终端的进程 -- 不能与用户进行交互的进程【输入、输出】
▶-j: 列出与作业控制相关的信息
-
kill -l 查看系统有哪些信号
-
kill -9 pid 杀死某个线程
3.3 getpid/getppid
- getpid - 得到当前进程的PID
pid_t getpid(void);
- getppid - 得到当前进程的父进程的PID
pid_t getppid(void);
3.3 练习题
- 编写程序,循环创建多个子进程,要求如下:
1.多个子进程是兄弟关系。
2.判断子进程是第几个子进程
画图讲解创建多个子进程遇到的问题
注意:若让多个子进程都是兄弟进程,必须不能让子进程再去创建新的子进程。
- 编写程序,测试父子进程是否能够共享全局变量
重点通过这个案例讲解读时共享,写时复制
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i = 0;
for(i = 0; i < 3; i++)
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
//sleep(1);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
}
}
return 0;
}
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i = 0;
for(i = 0; i < 3; i++)
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
//sleep(1);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
break;
}
}
//第1个子进程
if(i == 0)
{
printf("[%d]--[%d]: child\n", i, getpid());
}
//第2个子进程
if(i == 1)
{
printf("[%d]--[%d]: child\n", i, getpid());
}
//第3个子进程
if(i == 2)
{
printf("[%d]--[%d]: child\n", i, getpid());
}
//父进程
if(i == 3)
{
printf("[%d]--[%d]: father\n", i, getpid());
}
sleep(10);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make fork
cc fork.c -o fork
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./fork
father: pid==[27258],fpid==[22703]
father: pid==[27258],fpid==[22703]
father: pid==[27258],fpid==[22703]
[3]--[27258]: father
child: pid==[27261],fpid==[27258]
[2]--[27261]: child
child: pid==[27259],fpid==[27258]
[0]--[27259]: child
child: pid==[27260],fpid==[27258]
[1]--[27260]: child
父子进程不能共享全局变量
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 99;
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
g_val++;
}
else if(pid == 0)//子进程
{
sleep(1);//为了避免父进程还没有执行,子进程已经结束
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
printf("child: g_val==[%d]\n", g_val);
}
return 0;
}
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 99;
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
g_val++;
printf("[%p]", &g_val);
}
else if(pid == 0)//子进程
{
sleep(1);//为了避免父进程还没有执行,子进程已经结束
printf("[%p]", &g_val);
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
printf("child: g_val==[%d]\n", g_val);
}
return 0;
}
但是如果父子进程只是对全局变量做读操作,则父子进程在内存中只有一份,属于共享;
如果父子进程中的任何一个进程对该变量做修改操作,会在内存中拷贝一个副本,然后在这个副本上进行修改,修改完成以后映射回去。
---写时复制,读时共享。
4.exec函数族
4.1函数作用和函数介绍
有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。
使用方法一般都是在父进程里面调用fork创建处子进程,然后在子进程里面调用exec函数。
- execl函数
函数原型: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
参数介绍:
▶path: 要执行的程序的绝对路径
▶变参arg: 要执行的程序的需要的参数
▶arg:占位,通常写应用程序的名字
▶arg后面的: 命令的参数
▶参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行execl后面的代码,可以用perror打印错误原因。
execl函数一般执行自己写的程序。
- execlp函数
函数原型: int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数介绍:
▶file: 执行命令的名字,根据PATH环境变量来搜索该命令
▶arg:占位
▶arg后面的: 命令的参数
▶参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因。
execlp函数一般是执行系统自带的程序或者是命令。
execl.c
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: pid==[%d],fpid==[%d]\n", getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
execl("/usr/bin/ls", "ls", "-l", NULL);
perror("execl error");
}
return 0;
}
父进程先退出,子进程后退出
test.c
#include <stdio.h>
int main(int argc, char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++)
{
printf("[%d]:[%s]\n", i, argv[i]);
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make test
cc test.c -o test
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./test hello world in hao
[0]:[./test]
[1]:[hello]
[2]:[world]
[3]:[in]
[4]:[hao]
execl.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
//execl("/usr/bin/ls", "ls", "-l", NULL);
execl("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
perror("execl error");
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make execl
cc execl.c -o execl
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./execl
father:[2244], pid==[2243],fpid==[2017]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ child: pid==[2244],fpid==[1]
[0]:[test]
[1]:[hello]
[2]:[world]
[3]:[ni]
[4]:[hao]
execlp.c
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
//execl("/usr/bin/ls", "ls", "-l", NULL);
//execl("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
execlp("ls", "ls", "-l", NULL);
perror("execl error");
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make execl
cc execl.c -o execl
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./execl
father:[2253], pid==[2252],fpid==[2017]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ child: pid==[2253],fpid==[1]
总计 60
-rwxrwxr-x 1 cmt cmt 16176 10月 8 22:48 execl
-rw-rw-r-- 1 cmt cmt 665 10月 8 22:48 execl.c
-rwxrwxr-x 1 cmt cmt 16208 10月 8 21:13 fork
-rw-rw-r-- 1 cmt cmt 688 10月 8 21:13 fork.c
-rwxrwxr-x 1 cmt cmt 15960 10月 8 22:33 test
-rw-rw-r-- 1 cmt cmt 150 10月 8 22:33 test.c
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
//execl("/usr/bin/ls", "ls", "-l", NULL);
//execl("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
//execlp("ls", "ls", "-l", NULL);
execlp("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
perror("execl error");
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make execl
cc execl.c -o execl
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./execl
father:[2271], pid==[2270],fpid==[2017]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ child: pid==[2271],fpid==[1]
[0]:[test]
[1]:[hello]
[2]:[world]
[3]:[ni]
[4]:[hao]
execlp("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
修改为
execlp("./iitest", "test", "hello", "world" ,"ni" ,"hao", NULL);
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make execl
cc execl.c -o execl
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./execl
father:[2281], pid==[2280],fpid==[2017]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ child: pid==[2281],fpid==[1]
execl error: No such file or directory
test.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++)
{
printf("[%d]:[%s]\n", i, argv[i]);
}
sleep(100);
return 0;
}
execl.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
//execl("/usr/bin/ls", "ls", "-l", NULL);
//execl("./test", "test", "hello", "world" ,"ni" ,"hao", NULL);
//execlp("ls", "ls", "-l", NULL);
execlp("./test", "TESTING", "hello", "world" ,"ni" ,"hao", NULL);
//execlp("./iitest", "test", "hello", "world" ,"ni" ,"hao", NULL);
perror("execl error");
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make execl
cc execl.c -o execl
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./execl
father:[2363], pid==[2362],fpid==[2017]
child: pid==[2363],fpid==[2362]
[0]:[test]
[1]:[hello]
[2]:[world]
[3]:[ni]
[4]:[hao]
cmt 2363 1 0 07:46 pts/0 00:00:00 TESTING hello world ni hao
cmt 2364 2035 0 07:46 pts/2 00:00:00 ps -ef
execl: 一般用于执行用户自定义的应用程序.
execp: 一般用于执行系统命令
4.2 exec函数族原理介绍
exec族函数的实现原理图:
如:execlp(“ls”, “ls”, “-l”, NULL);
总结:
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。
4.3 exec函数练习
-
使用execl函数执行一个用户自定义的应用程序
-
使用execlp函数执行一个linux系统命令
注意:当execl和execlp函数执行成功后,不返回,并且不会执行execl后面的代码逻辑,原因是调用execl函数成功以后,exec函数指定的代码段已经将原有的代码段替换了。
4.4exec函数详解
(1)exec函数说明
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
(2)在Linux中使用exec函数族主要有以下两种情况
当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生。
如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
(3)exec函数族语法
实际上,在Linux中并没有exec函数,而是有6个以exec开头的函数族,下表列举了exec函数族的6个成员函数的语法。
这6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。
①查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径(pathname),而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
前4个取路径名做参数,后2个则取文件名做参数。
当指定filename做参数时:
a. 如果filename中包含/,则将其视为路径名
b. 否则就按PATH环境变量搜索可执行文件。
② 参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举(l)的方式,而另一种则是将所有参数整体构造成指针数组(v)进行传递。
在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vector)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。读者可以观察execl、execle、execlp的语法与execv、execve、execvp的区别。
③ 环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
(4)PATH环境变量说明
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码,PATH环境变量定义时目录之间需用用“:”分隔,以“.”号表示结束。PATH环境变量定义在用户的.profile或.bash_profile中,下面是PATH环境变量定义的样例,此PATH变量指定在“/bin”、“/usr/bin”和当前目录三个目录进行搜索执行码。
PATH=/bin:/usr/bin:.
export $PATH
(5)进程中的环境变量说明
在Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Shell进程堆栈中存放着该用户下的所有环境变量,使用execl、execv、execlp、execvp函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用execle、execve时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。
(6)exec函数族关系
事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用,调用关系如下图12-11所示:
(7)exec
调用举例如下
//NULL在此上下文中是一个指针常量,用于表示指针数组的结尾。在处理像exec*这样的函数时,这很有用,因为它们期望参数列表以NULL指针作为结束标志。
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
//"PATH=/bin:/usr/bin" 和 "TERM=console" 是字符串字面量,代表环境变量的名字和值。
//"PATH=/bin:/usr/bin" 表示路径变量PATH被设置为“/bin:/usr/bin”。
//"TERM=console" 表示终端类型变量TERM被设置为“console”。
//NULL在这里同样用于表示指针数组的结尾,这对于许多C函数来说都是必要的,因为它们使用NULL来识别参数列表的结束。
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
//"/bin/ps":这是你想要执行的程序的路径。这行代码想要执行系统上的 ps 命令,这个命令通常位于 /bin/ 目录下。
//"ps":这是传递给 /bin/ps 的第一个参数。当你在命令行上执行一个程序时,第一个参数(也叫做 argv[0])通常是该程序的名字。很多程序都依赖这个参数来获取自己的名字。
//"-o" 和 "pid,ppid,pgrp,session,tpgid,comm":这些是传递给 ps 的参数。特别是,-o 选项允许用户指定要显示的输出格式,后面的字符串则是这个格式的具体定义。在这里,这些参数意味着 ps 将会输出每个进程的 PID(进程ID)、PPID
//(父进程ID)、PGRP(进程组ID)、session(会话ID)、TPGID(前台进程组ID)和 comm(命令名或程序名)。
//NULL:这是参数列表的终止符。execl() 函数使用这个 NULL 来判断参数列表何时结束。
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
//execlp() 是 exec 函数家族中的另一个成员。这个函数用于执行程序,但与前面的 execl() 和 execv() 有一个主要区别:它会在环境变量 PATH 指定的目录中查找要执行的程序。这意味着你不必给出完整路径,只需要给出可执行文件的名字。
//"ps": 这是你想要执行的程序的名字。由于使用了 execlp(), 系统会在 PATH 环境变量中指定的目录列表中查找这个名字。
//"ps": 这是传递给 ps 的第一个参数。在命令行上执行程序时,第一个参数(也被称为 argv[0])通常是程序的名字。许多程序都依赖这个参数来知道自己是如何被调用的。
//"-o" 和 "pid,ppid,pgrp,session,tpgid,comm": 这些是传递给 ps 的其他参数。特别是,-o 选项允许用户指定要显示的输出格式,后面的字符串是这种格式的具体定义。
//NULL: 这是参数列表的终止符。execlp() 和其他 exec 函数使用这个 NULL 来确定参数列表何时结束。
//当 execlp() 函数成功时,当前进程的映像会被替换为指定的程序,并从该程序的 main 函数开始执行。如果 execlp() 失败,则函数会返回,并且进程不会被替换。
//总之,这行代码的目的是使用给定的参数执行名为 "ps" 的命令,并期望该命令位于 PATH 环境变量指定的某个目录中。
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
请注意exec函数族形参展开时的前两个参数,第一个参数是带路径的执行码(execlp、execvp函数第一个参数是无路径的,系统会根据PATH自动查找然后合成带路径的执行码),第二个是不带路径的执行码,执行码可以是二进制执行码和Shell脚本。
(8)exec函数族使用注意点
在使用exec函数族时,一定要加上错误判断语句。因为exec很容易执行失败,其中最常见的原因有:
① 找不到文件或路径,此时errno被设置为ENOENT。
② 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT。
③ 没有对应可执行文件的运行权限,此时errno被设置为EACCES。
(9)exec后新进程保持原进程以下特征
环境变量(使用了execle、execve函数则不继承环境变量);
进程ID和父进程ID;
实际用户ID和实际组ID;
附加组ID;
进程组ID;
会话ID;
控制终端;
当前工作目录;
根目录;
文件权限屏蔽字;
文件锁;
进程信号屏蔽;
未决信号;
资源限制;
tms_utime、tms_stime、tms_cutime以及tms_ustime值。
对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。
(10)execl函数
头文件 | #include<unistd.h> |
---|---|
功能 | 为进程重载0-3G的用户空间,可与fork函数搭配使用 |
语法 | int execl("绝对路径", “标识符”, “需要的参数”(需要多少传入多少) ,NULL); |
返回值 | 成功的话无返回值,失败的话返回 -1 |
我们来说明一下execl函数所需要的四个参数
参数 | 变量类型 | 解释 |
---|---|---|
绝对路径 | const char* | 文件存储的绝对路径,可通过pwd命令查看 |
标识符 | const char* | ① |
参数 | ------ | ② |
NULL | ------ | 最后这个必须传NULL,否则函数会报错 |
①标识符可以理解为编程时使用的“名字”,像命令 ls -a 中的ls就是标识符,是这个命令的“名字”,文件的文件名就是标识符,是这个文件的“名字”。
②参数很好理解,像命令 ls -a 中的 -a 就是参数,函数move(int a, int b)中的整型变量a和整形变量b就是参数
我们下面来写一个代码
所用函数:execl函数、fork函数
功能:创建三个子进程,并分别对三个子进程进行重载,第一个子进程实现使用火狐浏览器打开百度网页,第二个子进程创建一个名为huala的文件,第三个子进程显示当前目录下的文件,下图为使用火狐浏览器打开百度网页的大概流程,其余两个子进程类似该步骤
父进程通过fork函数创建子进程,子进程调用execl函数重载用户空间,来实现三个功能,以下是代码实现
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 3; i++)
{
pid = fork();
/*
这个地方要判断pid是否为0是因为fork函数的实现原理,fork函数最后的return 0是子进程进行
的,所以进入这个判断的是子进程,而子进程返回的pid就是0,如果这个地方不加上该判断,子进
程也会进入该for循环来创造进程,子又生孙孙又生子,而我们只希望父进程来创建三个子进程,
所以加上了该判断
*/
if (pid == 0)
{
break;
}
}
/*
首先父进程进入下面的三个判断,因为父进程pid大于0,所以会进入第一个判断,打印出父进程的
pid,然后我们用while循环一直sleep(1)来阻塞父进程,让子进程进入三个判断,因为子进程的pid
是0,所以会进入第二个判断,第一个子进程先进入判断,进入if(i == 0)用execl函数重载来实现功
能,firefox是命令标识符,www.baidu.com是参数,后面执行同样的步骤,也是父进程先进入判断,
之后两个进程分别进入判断并使用execl函数重载来实现功能
*/
if (pid > 0)
{
printf("parent pid %d\nsleeping..\n", getpid());
while (1)
{
sleep(1);
}
}
else if (pid == 0)
{
if (i == 0)
{
printf("child no.%d pid %d exec firefox..\n", i, getpid());
execl("/usr/bin/firefox", "firefox", "www.baidu.com", NULL);
}
if (i == 1)
{
printf("child no.%d pid %d touch files..\n", i, getpid());
execl("/usr/bin/touch", "touch", "huala", NULL);
}
if (i == 2)
{
printf("child no.%d pid %d exec ls -l..\n", i, getpid());
execl("/bin/ls", "ls", "-l", NULL);
}
}
return 0;
}
这样我们就实现了我们所想要达到的功能,记住exec函数一定要在fork函数之后执行
exec函数族的日常应用
其实exec在linux中的应用非常的广泛,就比如第一个终端的创建,还有终端下.c文件的执行,我们讲解一下这两个过程中exec函数族的应用
1.Linux中第一个终端的创建
具体过程:
1.init(1)是系统启动初始化后的第一个进程
2.当init进程初始化完成后系统会进行硬件检测,之后系统调用login函数
3.检查存放在/etc/passwd中的密码与用户输入的密码是否一致,一致的话init进程就调用fork函数创建子进程
4.子进程调用execl函数将子进程重载成bash终端,这样就实现了终端的创建
2.终端下.c文件的执行
在bash终端下我们先写一个world.c文件,然后将编译后的文件命名为app,看一下这个编译后的文件和bash终端的亲缘关系,我们可以通过命令 ps -ef|grep [进程名]
来查看对应该进程名的进程id与父进程id,大概流程如下
注意:
第一个数据是用户名,第二个数字是进程id,第三个数字是父进程id
我们可以发现,app的父进程就是bash终端,那么这是为什么呢?
原因就是bash终端调用了fork函数创建了一个子进程,子进程调用了execl函数,将文件app重载到了子进程中,所以app的父进程就是bash终端
(11)execlp函数举例
execlp.c源代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
if (fork() == 0)
{
if (execlp("/usr/bin/env", "env", NULL) < 0)
{//
perror("execlp error!");
return -1;
}
}
return 0;
}
编译 gcc execlp.c –o execlp。
执行 ./execlp,执行结果如下:
HOME=/home/test
DB2DB=test
SHELL=/bin/bash
……
由执行结果看出,execlp函数使执行码重生时继承了Shell进程的所有环境变量,其他三个不以e结尾的函数同理。
我们来详细讲解这段代码及其执行结果。
- 包含头文件
#include <stdio.h>
#include <unistd.h>
这里包含了两个头文件。stdio.h
用于输入输出函数,如 printf
和 perror
;unistd.h
提供了Unix标准函数,如 fork
和 execlp
。
- 主函数
int main()
{
开始定义主函数。
- 创建子进程
if (fork() == 0)
{
fork()
是UNIX系统中用来创建新进程的函数。这个函数的特点是调用一次,返回两次:一次是在父进程中,一次是在新创建的子进程中。在子进程中,fork()
返回0;在父进程中,它返回新创建的子进程的进程ID。
因此,if (fork() == 0)
这段代码的意图是仅在子进程中执行以下的代码块。
- 执行新程序
if (execlp("/usr/bin/env", "env", NULL) < 0)
{
execlp()
是 exec
函数家族的一员。它会替换当前进程的映像为新的程序映像。在这里,它尝试执行 /usr/bin/env
程序,同时将第一个参数(即 argv[0]
)设置为 "env"。
/usr/bin/env
是一个常用的Unix命令,用于打印出所有的环境变量。所以,当这个程序被执行时,它会输出所有设置的环境变量及其值,就像你在执行结果中看到的那样。
- 错误处理
perror("execlp error!");
return -1;
}
如果 execlp()
调用失败(例如,因为 /usr/bin/env
程序不存在),它将返回一个负值。这段代码会捕捉这种情况,并使用 perror
打印一个错误消息,然后使子进程返回一个 -1
的错误代码。
- 结束子进程代码块
这一部分结束了仅在子进程中执行的代码块。
- 主函数结束
return 0;
}
主函数结束,返回0表示成功。
- 执行过程
当你执行 ./execlp
,以下步骤将发生:
- 程序开始在父进程中运行。
- 当执行到
fork()
时,创建一个子进程。 - 子进程开始执行,并进入
if (fork() == 0)
代码块。 - 子进程试图用
/usr/bin/env
程序替换自己。如果成功,/usr/bin/env
程序将开始运行,并打印所有的环境变量。 - 一旦
env
完成并打印所有的环境变量,子进程将结束。 - 同时,父进程将继续执行,跳过
if (fork() == 0)
代码块,并直接结束。
这就是为什么当你执行 ./execlp
时,你会看到环境变量的列表输出。
将第一个参数(即 argv[0])设置为 "env"。什么意思?
在Unix和Linux系统中,当一个程序被执行时,它不仅仅接收到明确传递的参数,还会收到一个隐式的第一个参数,通常称为argv[0]
。这个参数是一个字符串,通常是用来启动程序的命令或程序的路径。
当你使用exec
系列的函数(例如execlp()
)来执行一个程序时,你可以明确地设置这个argv[0]
的值。这个值并不一定要与实际的程序名或路径匹配。事实上,它可以是任何字符串。程序可以访问这个argv[0]
值,并基于它进行不同的行为。
在给出的代码中:
execlp("/usr/bin/env", "env", NULL);
第一个参数指定了要执行的程序的路径,即/usr/bin/env
。
第二个参数指定了argv[0]
的值,即"env"。这里,我们简单地将它设置为与程序名相同的值,但它可以是任何其他的字符串。
很多程序会使用argv[0]
来查找自己是如何被调用的,这有时用于确定程序的行为。例如,一些Unix工具,如busybox
,会根据argv[0]
来确定要执行哪种功能。
(12)execle函数举例
利用函数execle,将环境变量添加到新建的子进程中去。
execle.c源代码如下:
#include <unistd.h>
#include <stdio.h>
int main()
{
/*命令参数列表,必须以 NULL 结尾*/
char* envp[] = { "PATH=/tmp","USER=sun",NULL };
if (fork() == 0) {
/*调用 execle 函数,注意这里也要指出 env 的完整路径*/
if (execle("/usr/bin/env", "env", NULL, envp) < 0)
{
perror("execle error!");
return -1;
}
}
return 0;
}
编译:gcc execle.c –o execle。
执行./execle,执行结果如下:
PATH=/tmp
USER=sun
可见,使用execle和execve可以自己向执行进程传递环境变量,但不会继承Shell进程的环境变量,而其他四个exec函数则继承Shell进程的所有环境变量。
总结示例exec.c:
/* exec函数族的语法 */
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main(int argc, char* argv[])
{
/* 字符串指针数组传递参数,使用包含v的exec函数参数 */
char* arg[] = { "ls","-a",NULL };
char* arg1[] = { "env",NULL }; //只用于execve函数
char* envp[] = { "NAME=amoscykl","EMAIL=xxxx@xx.com","PATH=/tmp",NULL };
char** ptr; //指向环境表
// 打印出环境表
printf("自定义环境表\n");
for (ptr = envp; *ptr != 0; ptr++)
printf("%s \n", *ptr);
printf("\n");
sleep(2);
/* 子进程调用execl函数 */
if (fork() == 0)
{
//child1
printf("1-----execl-----\n");
if (execl("/bin/ls", "ls", "-a", NULL) == -1)
{
perror("execl error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execv函数 */
if (fork() == 0)
{
//child2
printf("2-----execv-----\n");
if (execv("/bin/ls", arg) == -1)
{
perror("execv error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execlp函数 */
if (fork() == 0)
{
//child3
printf("3-----execlp-----\n");
if (execlp("ls", "ls", "-a", NULL) == -1)
{
perror("execlp error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execvp函数 */
if (fork() == 0)
{
//child4
printf("4-----execvp-----\n");
if (execvp("ls", arg) == -1)
{
perror("execvp error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execle函数 */
if (fork() == 0)
{
//child5
printf("5-----execle-----\n");
if (execle("/usr/bin/env", "env", NULL, envp) == -1) //使用自定义的环境表,并打印出自定义环境变量
{
perror("execle error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execve函数 */
if (fork() == 0)
{
//child6
printf("6-----execve-----\n");
if (execve("/usr/bin/env", arg1, envp) == -1) //使用自定义的环境表,并打印出自定义环境变量
{
perror("execve error!");
exit(1);
}
}
sleep(2);
printf("over!\n");
return 0;
}
运行结果:
5.进程回收
1.为什么要进行进程资源的回收
当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。
2.孤儿进程
- 孤儿进程的概念:
若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。
-
为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
-
模拟孤儿进程的案例
编写模拟孤儿进程的代码讲解孤儿进程,验证孤儿进程的父进程是否由原来的父进程变成了init进程。
orphan.c
//孤儿进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
sleep(5);
printf("father: [%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(20);
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make orphan
cc orphan.c -o orphan
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./orphan
child: pid==[33209],fpid==[33208]
father:[33209], pid==[33208],fpid==[22703]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ child: pid==[33209],fpid==[1440]
孤儿进程:systemd进程的进程 ID是1440(遇到其他很多情况ID是1),父进程先退出,子进程后退出变成孤儿进程,子进程由systemd进程接管,孤儿进程退出后就会被init进程回收
测试1: 孤儿进程测试
/************************************************************
>File Name : orphan.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月19日 星期四 20时53分41秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
if(pid > 0)
{
printf("parent: %d\n", getpid());
sleep(3);
}
return 0;
}
我们看到,子进程的父进程ID在3秒后变成了1,这说明父进程结束后,它变成了孤儿进程,并被init进程收养,使用kill命令基于可以杀死孤儿进程。
3.僵尸进程
- 僵尸进程的概念:
若子进程死了,父进程还活着,但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。
- 如何解决僵尸进程
▶由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
▶通过杀死其父进程的方法可以消除僵尸进程。杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
- 模拟僵尸进程的案例
编写模拟僵尸进程的代码讲解僵尸进程,验证若子进程先于父进程退出,而父进程没有调用wait或者waitpid函数进行回收,从而使子进程成为了僵尸进程。
僵尸进程:子进程先退出,父进程没有完成对子进程的回收,此时子进程就变成了僵尸进程
zomble.c
//僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
sleep(20);
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make zomble
cc zomble.c -o zomble
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./zomble
child: pid==[33307],fpid==[33306]
father:[33307], pid==[33306],fpid==[22703]
cmt 33306 22703 0 09:39 pts/0 00:00:00 ./zomble
cmt 33307 33306 0 09:39 pts/0 00:00:00 [zomble] <defunct>//表明这个进程是僵尸进程
cmt 33308 31486 0 09:39 pts/2 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make zomble
cc zomble.c -o zomble
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./zomble
child: pid==[33424],fpid==[33423]
UID PID PPID C STIME TTY TIME CMD
mt 33423 22703 0 09:52 pts/0 00:00:00 ./zomble
cmt 33424 33423 0 09:52 pts/0 00:00:00 [zomble] <defunct>
cmt 33425 31486 0 09:52 pts/2 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~$ kill -9 33424
cmt@cmt-VMware-Virtual-Platform:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
cmt 33423 22703 0 09:52 pts/0 00:00:00 ./zomble
cmt 33424 33423 0 09:52 pts/0 00:00:00 [zomble] <defunct>
cmt 33427 31486 0 09:53 pts/2 00:00:00 ps -ef
没有杀掉僵尸进程,可以杀死父进程来杀死
cmt@cmt-VMware-Virtual-Platform:~$ kill -9 33423
cmt@cmt-VMware-Virtual-Platform:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 33422 2 0 09:52 ? 00:00:00 [kworker/u256:1-events_unbound]
cmt 33432 31486 0 09:55 pts/2 00:00:00 ps -ef
如何解决僵尸进程:
不能使用kill -9杀死僵尸进程,原因是僵尸进程是一个死掉的进程;
应该使用杀死僵尸进程父进程的方法来解决僵尸进程;
原因是: 杀死其父进程可以让systemd进程领养僵尸进程,最后由systemd进程回收僵尸进程。
测试2: 僵尸进程测试
/************************************************************
>File Name : zombie.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月19日 星期四 20时54分20秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
if(pid > 0)
{
while(1)
{
printf("parent: %d\n", getpid());
sleep(1);
}
}
return 0;
}
我们可以通过ps命令查看僵尸进程
图中红色标出的三个地方Z+、[]、default都可以表明这是僵尸进程,另外Z+是进程类型的一个表示,可以通过 man ps 查看,我们可以通过 man ps 进入帮助手册,然后在命令行输入 /zombie 来搜索zombie相关的信息。
僵尸进程是不能用kill杀死的,因为kill命令是终止进程,而僵尸进程已经终止了。我们知道僵尸进程的资源需要由父进程去回收,那么我们在这种情况下如何回收僵尸进程的资源呢?方法就是杀死父进程,父进程被杀死后,由init接管子进程并回收子进程资源。
4.进程回收函数
4.1wait()函数
▶函数原型:
pid_t wait(int *status);
▶函数作用:
▷阻塞并等待子进程退出
▷回收子进程残留资源
▷获取子进程结束状态(退出原因)。
▶返回值:
▷成功:清理掉的子进程ID;
▷失败:-1 (没有子进程)
▶status参数:子进程的退出状态 -- 传出参数
▷WIFEXITED(status):为非0 -> 进程正常结束
WEXITSTATUS(status):获取进程退出状态
▷WIFSIGNALED(status):为非0 -> 进程异常终止
WTERMSIG(status):取得进程终止的信号编号。
一个进程在终止的时候会关闭所有的文件描述符,释放在用户空间分配的内存,但是它的PID还保留着,内核在其中保存了一些信息:如果进程是正常终止则保存进程退出状态;如果进程是异常终止,则保存导致该进程终止的那个信号。这个进程的父进程可以调用wait()或者waitpid()来获取这些信息,然后彻底清除这个进程。我们知道,一个进程的退出状态可以在shell中用特殊变量$?查看,因为shell进程是它的父进程,当它终止的时候shell调用wait()或waitpid()得到它的退出状态,同时彻底清除这个进程。父进程调用wait()函数可以回收子进程终止信息,wait()函数功能主要有三个:阻塞等待子进程退出;回收子进程残留资源;获取子进程退出状态(退出原因)。
(1)包含头文件及函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
/*
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
*/
(2)函数描述
wait()函数用于回收子进程,获取子进程的终止原因,如果子进程没有终止,那么将会阻塞等待子进程的终止。
(3)函数参数
- status:传出参数(C语言一级指针做输出)
WIFEXITED(status) /*wait if exited 等待是否退出*/
WEXITSTATUS(status) /*wait exit status 退出原因*/
WIFSIGNALED(status) /*wait if signaled 是否被信号杀死*/
WTERMSIG(status) /*wait term sugnaled 被几号信号杀死的*/
WCOREDUMP(status)
WIFSTOPPED(status)
WSTOPSIG(status)
WIFCONTINUED(status)
- 根据status判断子进程终止原因
WIFEXITED(status)判断子进程是否正常退出;
WIFEXITED(status)为真表示正常退出,使用WEXITSTATUS(status)获取退出状态;
WIFEXITED(status)非真,表示非正常退出,使用WIFSIGNALED(status)判断是否被信号杀死;
WIFSIGNALED(status)为真,表示是被信号杀死,使用WTERMSIG(status) 获取杀死进程的信号;
(4)函数返回值
- on success, returns the process ID of the terminated child; wait()函数成功返回终止的子进程的ID.
- on error, -1 is returned. 失败返回-1.
wait.c
//父进程调用wait函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
pid_t wpid = wait(NULL);
printf("wpid==[%d]\n", wpid);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(5);
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make wait
cc wait.c -o wait
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./wait
father:[34008], pid==[34007],fpid==[31486]
child: pid==[34008],fpid==[34007]
wpid==[34008]
阻塞5秒后,wpid打出来了,wpid是[34008],child的pid是[34008],说明已经回收掉了
wait.c
正常退出
//父进程调用wait函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
int status;
pid_t wpid = wait(&status);
printf("wpid==[%d]\n", wpid);
if(WIFEXITED(status))//正常退出
{
printf("child normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))//被信号杀死
{
printf("child killed by signal, signo==[%d]\n", WTERMSIG(status));
}
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(5);
return 9;
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make wait
cc wait.c -o wait
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./wait
father:[34132], pid==[34131],fpid==[31486]
child: pid==[34132],fpid==[34131]
wpid==[34132]
child normal exit, status==[9]
被信号杀死15是SIGTERM
cmt 34164 31486 0 12:42 pts/2 00:00:00 ./wait
cmt 34165 34164 0 12:42 pts/2 00:00:00 ./wait
cmt 34166 22857 0 12:42 pts/1 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ kill -15 34165
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./wait
father:[34165], pid==[34164],fpid==[31486]
child: pid==[34165],fpid==[34164]
wpid==[34165]
child killed by signal, signo==[15]
wait函数:
pid_t wait(int *status);
返回值:
>0: 回收的子进程的PID
-1: 没有子进程
参数:
status: 子进程的退出状态
if(WIFEXITED(status))
{
WEXITSTATUS(status)
}
else if(WIFSIGNALED(status))
{
WTERMSIG(status)
}
案例测试: wait()获取子进程退出原因
/************************************************************
>File Name : wait_test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月19日 星期四 22时45分28秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(3); /*子进程睡眠3秒,那么父进程中的wait函数会阻塞3秒,一直等到子进程退出*/
return 66; /*正常退出,这个值可以被WEXITSTATUS获取到,这个值是有范围的*/
/*exit(66); 也表示正常退出*/
}
if(pid > 0)
{
int status;
pid_t wpid = wait(&status);
printf("wpid: %d, cpid: %d\n", wpid, pid);
if(WIFEXITED(status)) /*进程正常退出,获取退出原因*/
{
printf("child exit because: %d\n", WEXITSTATUS(status));
}
else /*非正常退出*/
{
if(WIFSIGNALED(status)) /*为真表示被信号杀死*/
{
printf("signal is: %d", WTERMSIG(status));
}
else
{
printf("other...\n");
}
}
while(1)
{
sleep(3);
}
}
return 0;
}
我们首先演示一下子进程的正常退出,并获取退出状态,子进程的退出状态可以用return或者exit来传递。
在代码中,当 fork()
被调用时,它创建了一个子进程,这个子进程是父进程的副本。然后,两个进程(父进程和子进程)从 fork()
的返回位置开始并行执行。由于它们是并发执行的,理论上,父进程和子进程的代码都有机会首先运行。
然而,通常情况下,我们会看到子进程的输出先于父进程的输出,有以下几个原因:
-
调度器决策:操作系统的调度器可能决定让新创建的子进程先运行,尤其是当系统资源充足,且没有其他高优先级的任务时。
-
输出缓冲:
printf
函数通常使用缓冲输出。这意味着即使父进程或子进程先执行其printf
语句,输出可能不会立即显示在屏幕上或写入文件中。子进程在printf
之后进入sleep
状态,这可能导致其输出被刷新到屏幕上。而父进程在其printf
之后立即调用了wait()
,可能在子进程结束之前都不会刷新其输出缓冲区。 -
运行环境的影响:具体哪个进程先输出还取决于运行环境、系统的当前负载、其他运行中的进程和线程以及其他一些因素。
虽然在许多情况下,子进程的输出可能会先于父进程显示,但这并不是一个固定的规则。如果多次运行同一段代码,可能会看到不同的顺序,尤其是在高负载的系统或具有多个CPU核心的系统上。因此,除非有明确的同步机制,否则不能保证并发进程的执行顺序。
下面我们在子进程中增加一个循环,然后用信号杀死子进程
if (pid == 0)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(2); /*子进程睡眠3秒,那么父进程中的wait函数会阻塞3秒,一直等到子进程退出*/
while (1)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
重新编译运行,并开启另一个shell,使用 kill -9 杀死子进程
获取到杀死进程的信号,正好是9号信号,如果直接使用 kill pid 默认使用的是15号信号。
4.2waitpid()函数
- waitpid函数
▶函数原型:
pid_t waitpid(pid_t pid, int *status, in options);
▶函数作用
同wait函数
▶函数参数
参数:
pid:
pid = -1 等待任一子进程。与wait等效。
pid > 0 等待其进程ID与pid相等的子进程。
pid = 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
pid < -1 等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
status: 子进程的退出状态,用法同wait函数。
options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞。
▶函数返回值
>0:返回回收掉的子进程ID;
-1:无子进程
=0:参3为WNOHANG,且子进程正在运行。
- waitpid函数练习
使用waitpid函数完成对子进程的回收
waitpid.c
//父进程调用waitpid函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
int status;
pid_t wpid = waitpid(-1, &status, 0);
printf("wpid==[%d]\n", wpid);
if(WIFEXITED(status))//正常退出
{
printf("child normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))//被信号杀死
{
printf("child killed by signal, signo==[%d]\n", WTERMSIG(status));
}
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(200);
return 9;
}
return 0;
}
cmt 3458 2685 0 18:50 pts/0 00:00:00 ./waitpid
cmt 3459 3458 0 18:50 pts/0 00:00:00 ./waitpid
cmt 3460 3443 0 18:50 pts/1 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ kill -9 3459
mt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make waitpid
make: “waitpid”已是最新。
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./waitpid
father:[3459], pid==[3458],fpid==[2685]
child: pid==[3459],fpid==[3458]
wpid==[3459]
child killed by signal, signo==[9]
正常退出
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./waitpid
father:[3591], pid==[3590],fpid==[2685]
child: pid==[3591],fpid==[3590]
wpid==[3591]
child normal exit, status==[9]
//父进程调用waitpid函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
int status;
//pid_t wpid = waitpid(-1, &status, 0);
//-1表示等待任意一个子进程,WNOHANG表示不阻塞
pid_t wpid = waitpid(-1, &status, WNOHANG);
printf("wpid==[%d]\n", wpid);
if(wpid > 0)
{
if(WIFEXITED(status))//正常退出
{
printf("child normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))//被信号杀死
{
printf("child killed by signal, signo==[%d]\n", WTERMSIG(status));
}
}
else if(wpid == 0)
{
printf("child is living, wpid==[%d]\n", wpid);
}
else if(wpid == -1)
{
printf("no child is living, wpid==[%d]\n", wpid);
}
sleep(100);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(2);
return 9;
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make waitpid
cc waitpid.c -o waitpid
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./waitpid
father:[5251], pid==[5250],fpid==[4691]
wpid==[0]
child is living, wpid==[0]
child: pid==[5251],fpid==[5250]
cmt 5250 4691 0 20:43 pts/0 00:00:00 ./waitpid
cmt 5251 5250 0 20:43 pts/0 00:00:00 [waitpid] <defunct>
cmt 5252 5064 0 20:43 pts/1 00:00:00 ps -ef
//父进程调用waitpid函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
//创建子进程
//pid_t fork(void);
pid_t pid = fork();
if(pid < 0)//fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid > 0)//父进程
{
printf("father:[%d], pid==[%d],fpid==[%d]\n", pid, getpid(), getppid());
int status;
//pid_t wpid = waitpid(-1, &status, 0);
//-1表示等待任意一个子进程,WNOHANG表示不阻塞
while(1)
{
pid_t wpid = waitpid(-1, &status, WNOHANG);
printf("wpid==[%d]\n", wpid);
if(wpid > 0)
{
if(WIFEXITED(status))//正常退出
{
printf("child normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))//被信号杀死
{
printf("child killed by signal, signo==[%d]\n", WTERMSIG(status));
}
break;
}
else if(wpid == 0)//子进程还活着
{
printf("child is living, wpid==[%d]\n", wpid);
}
else if(wpid == -1)//没有子进程
{
printf("no child is living, wpid==[%d]\n", wpid);
break;
}
}
sleep(100);
}
else if(pid == 0)//子进程
{
printf("child: pid==[%d],fpid==[%d]\n",getpid(), getppid());
sleep(2);
return 9;
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ make waitpid
cc waitpid.c -o waitpid
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day5$ ./waitpid
child is living, wpid==[0]
wpid==[0]
...
child is living, wpid==[0]
wpid==[0]
child is living, wpid==[0]
wpid==[5393]
child normal exit, status==[9]
(1)包含头文件及函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
(2)函数描述
The waitpid() system call suspends execution of the calling process until a child specified by pid argument has changed state.
waitpid()
系统调用暂停调用进程的执行,直到由 pid
参数指定的子进程改变了状态。
(3)函数参数
- pid:
■小于 -1:meaning wait for any child process whose process group ID is equal to the absolute value of pid. 回收一个组的子进程,使用时把组ID(一般是父进程ID)传给pid参数,就可以使用waitpid()回收这个进程组的所有子进程。
■-1:meaning wait for any child process. 回收所有,任何子进程,这是最常用的取值,把所有子进程都回收。
■0:meaning wait for any child process whose process group ID is equal to that of the calling process. 回收和调用进程组ID相同的组内的子进程。
■大于0:meaning wait for the child whose process ID is equal to the value of pid. 回收指定的进程pid。
- status:传出参数,同wait()函数
- options:选项
■WNOHANG: return immediately if no child has exited. wait no hang,如果子进程没有结束,立即返回,不会挂起等待(wait函数如果子进程没有退出会阻塞等待)。如果options参数填0,那么和wait()函数一样会挂起等待子进程结束。
■WUNTRACED: also return if a child has stopped (but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.如果子进程已停止(但未通过 ptrace(2)
进行跟踪)则返回。即使没有指定此选项,仍会提供已停止的被跟踪子进程的状态。
■WCONTINUED: also return if a stopped child has been resumed by delivery of SIGCONT.如果一个已停止的子进程因接收到 SIGCONT 信号而恢复,则返回。
- 函数返回值
■on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. 如果设置了WNOHANG选项,并且没有子进程退出则返回0,如果有子进程退出则返回退出子进程的pid。
■On error, -1 is returned. 比如说没有子进程或子进程早就全部结束了,可能就会出错返回-1。
下面通过例子演示waitpid()函数的用法。
/************************************************************
>File Name : waitpid_test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月20日 星期五 16时31分35秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
printf("child: %d\n", getpid());
sleep(2);
}
if(pid > 0)
{
printf("parent: %d\n", getpid());
int ret = waitpid(-1, NULL, WNOHANG);
printf("ret: %d\n", ret);
while(1)
{
sleep(1);
}
}
return 0;
}
为什么使用了waitpid()函数还会产生僵尸进程呢,这是因为在waitpid()函数中使用了选项参数WNOHANG,而子进程中有一个睡眠函数,子进程睡眠的时候,父进程中waitpid()语句没有等到子进程结束就执行了,由于WNOHANG选项参数的存在,waitpid不会阻塞等待之进程结束,而是直接返回。当waitpid()返回父进程中后,子进程才结束,但是waitpid()已经执行完了,所以并没有回收子进程,子进程因此变成僵尸进程。
解决方法就是在一个循环中执行waitpid()函数,直到ret不等于0的时候说明子进程退出了,跳出循环。
5.回收多个子进程
上面使用wait()函数和waitpid()函数举的例子都是回收一个子进程,有时候我们可能需要回收多个子进程,下面介绍回收多个子进程的方法。
5.1 使用wait()回收多个子进程
首先使用wait()函数来回收多个子进程,我们可以在一个for循环中等待子进程的结束,创建了几个子进程就for循环等待几次,代码如下。
/************************************************************
>File Name : mutipwait.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月20日 星期五 17时23分57秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int i = 0;
pid_t pid;
for(i = 0; i < 5; i++)
{
pid = fork();
if(pid == 0)
{
printf("child: %d\n", getpid());
break;
}
}
sleep(i);
if(i == 5) /*只有父进程可以执行到i=5*/
{
for(i = 0; i < 5; i++)
{
pid_t wpid = wait(NULL);
printf("wpid: %d\n", wpid);
}
while(1)
{
sleep(1);
}
}
return 0;
}
编译运行,可以看到所有子进程都被回收。
5.2使用waitpid()回收多个子进程
如果使用waitpid()函数,可以借助函数的参数和返回值去判断每个子进程是否回收成功。
/************************************************************
>File Name : mutipwaitpid.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月20日 星期五 17时45分39秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int i = 0;
pid_t pid;
for(i = 0; i < 5; i++)
{
pid = fork();
if(pid == 0)
{
break;
}
}
if(i == 5) /*只有父进程可以执行到i=5*/
{
printf("parent: %d\n", getpid());
while(1) /*无限循环保证所有子进程全部回收*/
{
pid_t wpid = waitpid(-1/*回收任何子进程*/, NULL, WNOHANG);
if(wpid == -1)
{
break; /*如果返回-1说明已经没有子进程了,退出循环*/
}
if(wpid > 0)
{
printf("wpid: %d\n", wpid); /*打印被回收的子进程的ID*/
}
}
while(1)
{
sleep(1);
}
}
if(i < 5) /*说明是子进程*/
{
printf("no. %d child: %d\n", i, getpid());
}
return 0;
}
编译执行,可以看到所有进程都被回收了
参考资料: