一个进程执行结束,但是进程的PCB还没有被系统释放,因为进程结束后,在PCB中还要保存进程退出码,以备其父进程获取其退出码,那么就是父进程未结束,子进程结束,所以此时父进程没有获取子进程的退出码。即一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,释放所占资源,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
我们可以用一个代码来看一下僵死进程长什么样子。我们fork一个子进程,让它睡眠10s结束,父进程睡眠20s,这样子进程会比父进程先结束,子进程就会变成一个僵死进程。那么代码如下:
# include
# include
# include
# include
int main()
{
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)//子进程返回PID=0
{
printf("i am child\n");
sleep(10);
printf("child over\n");
}
else//父进程返回子进程的PID
{
printf("i am father\n");
sleep(20);
printf("father over");
}
exit(0);
}
我们将上面的代码放到后台运行,在前台查看进程运行状态,可以用ps查看进程状态,也可以用top查看整体状态。
我们需要清楚一个概念:任何一个子进程(除init进程,它是所有进程的父进程)在exit()后,并非马上就消失掉,会留下一个僵尸进程(Zombie)的数据结构,等待父进程处理,这是每个进程都要经历的阶段,如果父进程处理及时,那么我们看不到,但不代表它没有经历这个阶段。
(1)僵死进程的危害:
先结束的子进程不会自己释放占用的PCB资源,如果父进程不调用wait/waitpid,那么这段信息不会释放,PID就会被占用,但是一个系统能使用的进程号是有限的,如果产生大量的僵死进程,那么系统就不能产生新的进程了,系统就会崩溃,资源被一直浪费,这就是危害。
(2)孤儿进程:
一个父进程退出,但是它的子进程还在运行,那么这些进程称为孤儿进程,内核会将孤儿进程的父进程设为init进程,孤儿进程被init进程(进程号为1)处理,init会循环的wait()这些孤儿进程,所以孤儿进程没有危害。
当一个进程正常或异常终止时,内核就像其父进程发送SIGCHLD信号,子进程终止是一个异步事件,即可以在父进程运行的任何时候发生,所以这种信号是内核向父进程发的异步通知,父进程可以选择忽略该信号,或者提供一个该信号发生时被调用执行的函数,我们后面会对信号量处理僵死进程
进行讲解。主要处理僵死进程的执行函数是wait和waitpid函数。头文件为:# include
1.函数原型:
pid_t wait(int *statloc)
2.参数:
statloc是一个整型指针,如果传入指针进去,那么进程的终止状态就会存放在它所指向的内存空间内;如果传入空指针,表示不关心终止状态。
成功返回子进程的PID,如果没有子进程调用会失败返回-1,同时errno被置为ECHILD。
3.作用:
父进程一旦调用了wait,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。 那么如果父进程调用wait()阻塞且有多个子进程,那么在其一个子进程终止时,wait就返回子进程的PID,所以wait函数总是了解哪一个子进程终止了。
如果有一个父进程有多个子进程,只要有一个子进程终止,wait就返回,那么如果要等待一个指定的进程终止再返回,应该怎么办呢?就产生了waitpid函数。
1.函数原型:
pid_t waitpid(pid_t pid,int *statloc,int options)
2.参数说明:
取值 | 含义 |
---|---|
pid==-1 | 等待任一进程,和wait函数效果一样 |
pid>0 | 等待僵死进程ID和pid相等时返回 |
pid==0 | 等待其组ID等于调用进程组ID的任一进程 |
pid<-1 | 等待其组ID等于pid绝对值的任一进程 |
常量 | 说明 |
---|---|
WCON TINUED | 若实现支持作业控制,那么由pid指定的任一进程在暂停后已经继续,但其状态尚未报告,则返回其状态 |
WNOHANG |
若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,返回值为0 |
WUNTRACEO | 若某实现支持作业控制,而由pid指定的任一进程已处于暂停状态,并且其状态自暂停还未报告过,返回其状态 |
3.作用:
在父进程调用时可以通过options选项使父进程不阻塞,也可以处理通过pid指定的僵死进程。
1.联系:
如果将waitpid函数的参数设置为:pid=-1,statloc=和wait一样的参数,option=0即waitpid(-1,statloc,0),它的功能就和wait()函数一样。所以在内核源码中我们可以看到下面的程序段:
static inline pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}
这就说明wait函数就是经过包装的waitpid函数。那么当我们调用wait(NULL)就等价于waitpid(-1,NULL,0)
2.区别:
我们可以对上面写的代码,进行两种办法的僵死进程处理,验证父进程是否阻塞。
1.wait函数:
直接在父进程里面进行wait的调用即可,参数为NULL。
# include
# include
# include
# include
# include
int main()
{
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
printf("i am child\n");
sleep(10);
printf("child over\n");
}
else
{
pid_t id=wait(NULL);//处理僵死进程
printf("i am father\n");
sleep(20);
printf("father over");
}
exit(0);
}
运行结果:
2.waitpid函数:
我们指定处理fork()之后PID为子进程的僵死进程,设置为不阻塞的即options=WNOHANG,那么在父进程中调用waitpid(pid,NULL,WNOHANG)即可。
# include
# include
# include
# include
# include
int main()
{
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
printf("i am child\n");
sleep(5);
printf("child over\n");
}
else
{
pid_t id;
do//父进程循环检测僵死进程,并不会阻塞在这不动
{
id=waitpid(pid,NULL,WNOHANG);//如果最后一个参数options值为0,那么就会阻塞,和wait一样
if(id==0)
{
printf("child run\n");
sleep(1);
}
}while(id==0);
printf("i am father\n");
sleep(10);
printf("father over");
}
exit(0);
}
fork创建子进程后,子进程执行的还是父进程的指令,意义不大。所以为了让一个进程去执行另一份程序。即fork()之后,子进程进行进程替换执行另外一份代码,父进程继续执行本身的代码,如父进程执行main.c,子进程执行test.c。
在fork之后,调用一种exec函数可以执行另一个程序,当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,新程序从main函数开始执行。注意调用exec并不创建新进程,进程PID不会变,exec只是用一个全新的程序替换了当前进程的正文,数据,堆栈。
fork()之后,exec之前两个进程用的是相同的物理空间,子进程的代码段,数据段,堆栈都是指向父进程的物理空间(物理页面为只读模式),也就是说,两者的虚拟空间不同,但对应的物理空间是一样的,这就是写时拷贝技术;如果不是exec,内核会为子进程的数据段,堆,栈分配物理空间,而代码段继续共享父进程的物理空间,而如果是因为exec,由于两者的代码不同,子进程的代码段也会分配到独立的物理空间。
那么在forl之前打开的文件,fork之后,文件描述符父子进程共享,在文件共享已经详细说明,所以当一个程序调用 exec 执行新程序时,在程序中已被打开的文件,其在新程序中仍保持打开。这就是说,已打开文件描述符能通过 exec 被传送给新程序,并且这些文件的指针也不会被 exec 调用改变。
有6种不同的exec函数可供使用,作为UNIX进程控制原语,fork创建进程,exec可以执行新进程,exit处理终止,wait处理僵尸进程。6个进程替换函数原型如下:
# include
int execl(const char* pathname,const char*argv0,char*argv1,…(char*)0);
int execv(const char*pathname,char*argv[]);
int execle(const char*pathname,char*argv()…,(char*)0,char*envp[]);
int execve(const char* pathname,char* const argv[],char* const envp[]);
int execlp(const char* filename,const char* arg()…(char*)0);
int execvp(const char* filename,char* const argv[]);
6个替换函数的参数有所不同,要注意区分,主要从下面几个方面:
(1)指定替换文件的方式
(2)传参列表;l表示list,v表示vector
(3)环境变量的传递
总结来说:根据函数名中的字符来判断,p表示该函数取filename作为参数,并且要用PATH环境变量寻找可执行文件,字母l表示该函数取一个参数,v表示该函数去一个argv[]矢量,最后,字母e表示该函数取envp[]数据,不适用当前的环境变量。
还需要知道只有execve是内核的系统调用,另外5个是库函数,都要调用系统调用
那我们可以根据这几个特性来列出和画出6个函数的参数,关系:
参数:
函数 | pathname | filename | 参数表 | argv[] | environ | envp[] |
---|---|---|---|---|---|---|
execl | √ | √ | √ | |||
execlp | √ | √ | √ | |||
execle | √ | √ | √ | |||
execv | √ | √ | √ | |||
execvp | √ | √ | √ | |||
execve | √ | √ | √ |
创建子进程,实现父进程执行execl.c,子进程执行test.c,子进程打印父进程传递的参数,使用execl替换函数。filename是替换进程的路径不是源文件路径,
,那么我们实现代码:
# include
# include
# include
# include
int main()
{
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
printf("i am child:pid=%d\n",getpid());
execl("./test","./test","Hello","Linux",(char*)0);//传递参数
printf("child last code\n");
}
else
{
printf("i am father\n");
sleep(10);
printf("father over");
}
exit(0);
}
# include
# include
# include
# include
int main(int argc,char* argv[])
{
printf("i am child new code:my pid=%d\n",getpid());
int i=0;
for(;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}
}
加油哦!。