十二、僵死进程 && 进程替换

文章目录

    • 一、僵死进程
      • (一)什么是僵死进程
        • 1. 僵死进程的产生
        • 2. 查看僵死进程
        • 3. 僵死进程的危害&&区分孤儿进程和僵死进程
      • (二)处理僵死进程
        • 1.wait函数
        • 2.waitpid函数
        • 3.两者的区别
        • 4.两种办法处理僵死进程
    • 二、进程替换
      • (一)进程替换:
      • (二)进程替换函数:
      • (三)函数区别:
      • (四)举例

一、僵死进程

(一)什么是僵死进程

1. 僵死进程的产生

一个进程执行结束,但是进程的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);
}

2. 查看僵死进程

我们将上面的代码放到后台运行,在前台查看进程运行状态,可以用ps查看进程状态,也可以用top查看整体状态。

  • ps查看
    十二、僵死进程 && 进程替换_第1张图片
    僵死进程的表示为defunct。
  • top查看僵死进程
    在这里插入图片描述

我们需要清楚一个概念:任何一个子进程(除init进程,它是所有进程的父进程)在exit()后,并非马上就消失掉,会留下一个僵尸进程(Zombie)的数据结构,等待父进程处理,这是每个进程都要经历的阶段,如果父进程处理及时,那么我们看不到,但不代表它没有经历这个阶段。

3. 僵死进程的危害&&区分孤儿进程和僵死进程

(1)僵死进程的危害: 先结束的子进程不会自己释放占用的PCB资源,如果父进程不调用wait/waitpid,那么这段信息不会释放,PID就会被占用,但是一个系统能使用的进程号是有限的,如果产生大量的僵死进程,那么系统就不能产生新的进程了,系统就会崩溃,资源被一直浪费,这就是危害。
(2)孤儿进程:一个父进程退出,但是它的子进程还在运行,那么这些进程称为孤儿进程,内核会将孤儿进程的父进程设为init进程,孤儿进程被init进程(进程号为1)处理,init会循环的wait()这些孤儿进程,所以孤儿进程没有危害。

(二)处理僵死进程

当一个进程正常或异常终止时,内核就像其父进程发送SIGCHLD信号,子进程终止是一个异步事件,即可以在父进程运行的任何时候发生,所以这种信号是内核向父进程发的异步通知,父进程可以选择忽略该信号,或者提供一个该信号发生时被调用执行的函数,我们后面会对信号量处理僵死进程进行讲解。主要处理僵死进程的执行函数是wait和waitpid函数。头文件为:# include

1.wait函数

1.函数原型:

pid_t wait(int *statloc) 

2.参数: statloc是一个整型指针,如果传入指针进去,那么进程的终止状态就会存放在它所指向的内存空间内;如果传入空指针,表示不关心终止状态。
成功返回子进程的PID,如果没有子进程调用会失败返回-1,同时errno被置为ECHILD。
3.作用:父进程一旦调用了wait,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。 那么如果父进程调用wait()阻塞且有多个子进程,那么在其一个子进程终止时,wait就返回子进程的PID,所以wait函数总是了解哪一个子进程终止了。

2.waitpid函数

如果有一个父进程有多个子进程,只要有一个子进程终止,wait就返回,那么如果要等待一个指定的进程终止再返回,应该怎么办呢?就产生了waitpid函数。
1.函数原型:

pid_t waitpid(pid_t pid,int *statloc,int options)

2.参数说明:

  • pid是进程PID,通过它可以等待一个特定的僵死进程结束,取值如下:
取值 含义
pid==-1 等待任一进程,和wait函数效果一样
pid>0 等待僵死进程ID和pid相等时返回
pid==0 等待其组ID等于调用进程组ID的任一进程
pid<-1 等待其组ID等于pid绝对值的任一进程
  • statloc参数和wait一样,options参数可以进一步控制waitpid的操作,参数可以取0,也可以或下面的常量:
常量 说明
WCON TINUED 若实现支持作业控制,那么由pid指定的任一进程在暂停后已经继续,但其状态尚未报告,则返回其状态
WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,返回值为0
WUNTRACEO 若某实现支持作业控制,而由pid指定的任一进程已处于暂停状态,并且其状态自暂停还未报告过,返回其状态
  • 返回值,成功返回子进程PID;如果进行不阻塞处理,那么成功返回0;如果没有子进程,或进程组不存在,指定进程不符合要求都会出错,返回-1。

3.作用:在父进程调用时可以通过options选项使父进程不阻塞,也可以处理通过pid指定的僵死进程。

3.两者的区别

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.区别:

  • 在一个进程终止前,wait函数使其调用者阻塞,而waitpid有一个选项,即options=WNOHANG,可以使调用者不阻塞。
  • wait在第一个子进程终止就返回,而waitpid可以根据参数pid来指定一个特定的进程,控制所等待的进程。
  • waitpid支持作业控制。

4.两种办法处理僵死进程

我们可以对上面写的代码,进行两种办法的僵死进程处理,验证父进程是否阻塞。

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张图片
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);
}

运行结果:
十二、僵死进程 && 进程替换_第3张图片

二、进程替换

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)指定替换文件的方式

  • 前四个函数取路径名作为参数,表示替换的进程路径;
  • 后两个文件取文件名作为参数,当指定filename作为参数时:如果filename种包含/,则将其视为路径名。否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件,PATH变量包含了一张目录表,目录之间用冒号【:】分隔,如PATH=/bin:/usr/bin:/use/local/bin/:.这个就有三个目录。

(2)传参列表;l表示list,v表示vector

  • execl,ececlp,execle要求程序的每个命令行参数都说明为一个单独的参数(如“hello"),以空指针(char*)0结尾。
  • 另外三个execv,execvp,execve需要先构造一个指向各参数的指针数组,然后将数组的指针地址作为这三个函数的参数。

(3)环境变量的传递

  • 以e结尾的两个函数execle,execve传递一个指向环境字符串指针数组的指针。其他四个函数则使用进程中的environ变量为新程序复制现有的环境。

总结来说:根据函数名中的字符来判断,p表示该函数取filename作为参数,并且要用PATH环境变量寻找可执行文件,字母l表示该函数取一个参数,v表示该函数去一个argv[]矢量,最后,字母e表示该函数取envp[]数据,不适用当前的环境变量。

还需要知道只有execve是内核的系统调用,另外5个是库函数,都要调用系统调用 那我们可以根据这几个特性来列出和画出6个函数的参数,关系:
参数:

函数 pathname filename 参数表 argv[] environ envp[]
execl
execlp
execle
execv
execvp
execve

关系:
十二、僵死进程 && 进程替换_第4张图片

(四)举例

创建子进程,实现父进程执行execl.c,子进程执行test.c,子进程打印父进程传递的参数,使用execl替换函数。filename是替换进程的路径不是源文件路径,,那么我们实现代码:

  1. execl.c
# 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);
}
  1. test.c
# 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]);
    }
}

运行:
十二、僵死进程 && 进程替换_第5张图片

加油哦!。

你可能感兴趣的:(Linux)