Linux中进程控制讲解--进程创建/进程等待/进程终止/进程程序替换

1. 进程创建

在进程中fork函数创建子进程。返回值小于0,子进程创建失败;大于0,返回给父进程;等于0,返回给子进程。
原理:子进程拷贝父进程的PCB。

int vfork函数—>创建子进程的函数
在这里插入图片描述

说明几点

  • vfork函数创建出来的子进程拷贝父进程的PCB,并且子进程PCB当中的内存指针指向了子父进程的进程虚拟地址空间,言外之意,父子进程共用一块虚拟地址空间。
  • 存在调用栈混乱的问题。理由:假设上图中父进程的func1()先压栈,由于子进程和父进程是抢占式占有进程虚拟地址空间,所以下个可能子进程sum1()进行压栈,当func1()调用完毕后要出栈的话,sum1有可能是死循环而导致func1无法出栈,所以导致调用栈混乱问题。
  • vfork是如何解决这个问题?答案是让子进程先运行,再让父进程运行。
  • 目前在工业上很少见了。

2. 进程终止

  • 含义:进程的退出;
  • 场景:1. 程序跑完了所有代码,从main函数的return返回。一种是代码跑完了,结果正确;一种是代码跑完了,结果不正确。
       2. 程序没有跑完,直接崩溃掉。需要关注的是:6号信号–>double free; 11号信号–>解引用空指针,访问越界;
  • 进程退出的方法:
  1. main函数的return返回;
  2. 引用库函数exit函数,终止掉一个进程;
      2.2 void exit(int status); status:进程在退出的时候,可以指定退出码是多少。
      2.3 echo $? ----> 能够获取最后一个终止进程的退出码。
  3. 系统调用_exit函数,终止掉一个进程。
    实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
  
 int main()
 {
    
    
		printf("---begin---\n");
		exit(1);
		printf("---end---\n");                                                                       
		return 0;
 }

输出结果:
在这里插入图片描述
由此能看到exit函数提前终止掉了一个进程。其中用_exit函数也是和exit一样。用echo $?查看得到终止码如下:
在这里插入图片描述
4. exit和_exit的区别
在这里插入图片描述
读缓冲区和写缓冲区,exit这样的函数会刷新缓冲区;但_exit函数,并不会刷新缓冲区。exit是库函数,_exit是系统调用函数。
原因:缓冲区是c库当中维护的,并不是内核维护的。如果直接调用_exit函数,直接执行了内核代码,就不会刷新缓冲区了;如果调用了exit函数,在进程结束之前,会先将缓冲区当中的数据进行刷新,在结束进程。

  1. 什么情况下会刷新缓冲区
  • 从main函数的return返回会刷新缓冲区;
  • \n也会刷新缓冲区;
  • fflush函数,强制刷新;
  • 调用exit也会刷新缓冲区;

自定义清理函数

  • int atexit(void (*function)(void));
  • 参数是函数指针,接受返回值为void,参数为void的函数。
  • 作用:调用atexit函数,参数为一个函数的地址,会先将这个函数的地址保存下来,等待进程退出时,在来调用这个函数。图例如下:

3. 进程等待

进程等待的必要性:为了防止产生僵尸进程。
进程等待的方法
在此之前需要了解函数的参数类型,分为3类,第一类是输入型参数–>参数是给函数内部使用的,为函数传递参数值。第二类是输出型参数–>参数是在函数内存赋予值的,在函数的外部进行使用。第三类是输入输出型参数,是以上两种的结合体。

pid_t wait(int status)---->status:输出型参数*

status是整形指针类型,在操作系统中占用4个字节,在进程等待输出型参数时只用到后面两个字节。例如下图:
在这里插入图片描述

在这两个字节中,第一个字节表示退出码,第二个字节首个比特位表示coredump标志位,后7位代表信号位,例如下图:
在这里插入图片描述

正常退出时,只用到了退出码,后面8个比特位没有用到,全部为0;

如何判断一个程序是否正常退出?
只需要判断是否接受到信号。将status的后7位按位与0x7f(1111111),如果信号按位与的结果大于0的数字,程序异常退出,有终止信号产生;如果按位与后的结果等于0,程序正常退出。

如何获取coredump标志位?
方法:(status >> 7)& 0x1 ----> 等于0,表示没有coredump标志位;等于1,有coredump标志位。

如何获取退出码?
方法:(status >> 8)& 0xff

使用方式:
创建一个子进程,子进程正常逻辑,父进程调用wait函数来进行等待,当子进程退出时,父进程由于在等待,所以子进程就不会变成僵尸进程。

结论:

  • wait函数在等待子进程,所以子进程没有变成僵尸进程;
  • 父进程当中一开始调用wait函数,父进程就会被阻塞在wait函数中,看到的现象是当父进程执行的wait函数之后,父进程似乎“卡死”在wait函数中。其中可以用pstack命令查看父进程的调用堆栈,可以看出目前父进程在执行什么代码。语法:pstack [pid]。
  • 子进程不退出,wait函数调用不返回。

父进程在等在等待的状态就是阻塞,阻塞就是当调用函数需要等待一定条件成熟时,则进行返回;条件不成熟时,则一直等待。对于调用函数进入到阻塞状态的进程时,什么代码都执行不了,除非当前阻塞的这个函数返回之后,才能去执行其他代码。还有另一种状态是非阻塞状态,当调用函数需要等待一定条件成熟时,条件成熟则返回;条件不成熟,也会报错并返回。

子进程和父进程是一个独立的进程,子进程退出时,父进程如何知道?子进程有是如何通知父进程的?
子进程退出的时候,会给父进程发送一个信号,信号叫做SIGCHLD信号,进程对SIGCHLD信号默认是忽略处理;因此僵尸进程产生的原因是子进程退出,父进程收到了子进程的退出信号SIGCHLD,但是父进程对于该信号是忽略处理的,所以导致子进程资源无法释放。在进程等待时,由于有wait函数,它将父进程收到的SIGCHLD信号进行处理,子进程才可以正常退出。

waitpid函数
函数模型:pid_t waitpid(pid_t pid, int* status, int options);
参数含义:

  • pid:告诉waitpid函数,需要等待的子进程的进程号;pid == -1:表示等待任意子进程,pid > 0:表示等待指定子进程,子进程的进程号就为传入的pid的值。
  • status:和wait函数当中的status参数含义一致
  • options:设置waitpid函数是阻塞还是非阻塞,其中0表示阻塞;WNOHANG表示调用非阻塞,当子进程没有推出的时候,waitpid函数会报错返回。
  • waitpid(pid > 0, status, 0) ==> 相当于wait函数,wait函数就是调用waitpid函数实现的。

进程等待测试:获取status当中字段的值(退出码、信号、coredump表示位),非阻塞状态下的值。代码如下:

#include <stdio.h>                                                                                   
#include <unistd.h>    
//调用wait函数用到的头文件    
#include <sys/wait.h>    
#include <stdlib.h>    
    
int main()    
{
    
        
    pid_t pid = fork();    
    if( pid < 0)    
    {
    
        
        perror("fork");    
        return 0;     
    }    
    else if(pid == 0)    
    {
    
        
        //child    
        int count = 10;    
        while(1)    
        {
    
        
           if(count <= 0)    
           {
    
        
               break;    
           }    
           printf("begin--->> i am child pid=[%d], ppid=[%d]\n", getpid(), getppid());    
           count--;    
           sleep(1);    
        } 
        exit(10);
    }
    else
    {
    
    
        //father  pid >0
        printf("------------>i am father pid=[%d], ppid=[%d]\n", getpid(),getppid());
        int status;
        wait(&status);
		printf("exit_code:%d\n", (status >> 8) & 0xff);
		
        while(1)
        {
    
    
            printf("i am father pid=[%d], ppid=[%d]\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;                                                                                        
}

首先打印status当中的退出码,在本例中引用exit函数并设置退出码为10,将(status >> 8) & 0xff就可显示退出码的值。结果如下:
在这里插入图片描述
由结果能看出来父进程先打印一条i am father并打印了pid和ppid,然后父进程在阻塞状态等待子进程的退出,在这里设置了子进程最后退出时的退出码是10,由结果能看出来当子进程循环了10次后开始退出,然后父进程wait函数等到了退出码并将其打印,后续就是父进程一直循环打印。

然后获取信号,修改代码如下,将子进程中的break跳出条件注释掉,这样就不会产生退出码,发送一个9号强杀信号到父进程,并在父进程中接受打印。

#include <stdio.h>                                                                                   
#include <unistd.h>    
//调用wait函数用到的头文件    
#include <sys/wait.h>    
#include <stdlib.h>    
    
int main()    
{
    
        
    pid_t pid = fork();    
    if( pid < 0)    
    {
    
        
        perror("fork");    
        return 0;     
    }    
    else if(pid == 0)    
    {
    
        
        //child    
        int count = 10;    
        while(1)    
        {
    
        
           //if(count <= 0)    
           // {    
           //     break;    
           // }    
           printf("begin--->> i am child pid=[%d], ppid=[%d]\n", getpid(), getppid());    
           count--;    
           sleep(1);    
        } 
        exit(10);
    }
    else
    {
    
    
        //father  pid >0
        printf("------------>i am father pid=[%d], ppid=[%d]\n", getpid(),getppid());
        int status;
        wait(&status);
		//printf("exit_code:%d\n", (status >> 8) & 0xff);
		printf("sig_code : %d\n", status & 0x7f);  
		 
        while(1)
        {
    
    
            printf("i am father pid=[%d], ppid=[%d]\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;                                                                                        
}
  1. 结果是一直在子进程当中循环:
    在这里插入图片描述

  2. 查看当前进程,对应上图,其中4408位子进程,4407位父进程。
    在这里插入图片描述

  3. 发送kill -9 4408信号到父进程,查看结果如下:
    在这里插入图片描述
    获取coredump标志位,由于对空指针的解引用会导致程序崩溃,因此在子进程中对空指针解引用,产生coredump文件,修改代码如下,

int main()    
{
    
        
    pid_t pid = fork();    
    if( pid < 0)    
    {
    
        
        perror("fork");    
        return 0;     
    }    
    else if(pid == 0)    
    {
    
        
        //child    
        int count = 10;
        sleep(1);    
        int* lp = NULL;
        *lp = 10;
        while(1)    
        {
    
        
           //if(count <= 0)    
           // {    
           //     break;    
           // }    
           printf("begin--->> i am child pid=[%d], ppid=[%d]\n", getpid(), getppid());    
           count--;    
           sleep(1);    
        } 
        exit(10);
    }
    else
    {
    
    
        //father  pid >0
        printf("------------>i am father pid=[%d], ppid=[%d]\n", getpid(),getppid());
        int status;
        wait(&status);
		//printf("exit_code:%d\n", (status >> 8) & 0xff);
		printf("sig_code : %d\n", status & 0x7f);  
		printf("coredump_code:%\n", (status >> 7) & 0x1);   
		
        while(1)
        {
    
    
            printf("i am father pid=[%d], ppid=[%d]\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;                                                                                        
}

在这里插入图片描述
从结果能看出来,解引用后子程序崩溃,发送给父进程11号信号,coredump标志位的结果也为1,和我们前面描述的是一样的。

最后验证非阻塞状态

int main()    
{
    
        
    pid_t pid = fork();    
    if( pid < 0)    
    {
    
        
        perror("fork");    
        return 0;     
    }    
    else if(pid == 0)    
    {
    
        
        //child    
        int count = 10;
        while(1)    
        {
    
        
           //if(count <= 0)    
           // {    
           //     break;    
           // }    
           printf("begin--->> i am child pid=[%d], ppid=[%d]\n", getpid(), getppid());    
           count--;    
           sleep(1);    
        } 
        exit(10);
    }
    else
    {
    
    
        //father  pid >0
        printf("------------>i am father pid=[%d], ppid=[%d]\n", getpid(),getppid());
        int status;
        waitpid(pid, &status, WNOHANG);  
		//printf("exit_code:%d\n", (status >> 8) & 0xff);
		printf("sig_code : %d\n", status & 0x7f);  
		printf("coredump_code:%\n", (status >> 7) & 0x1);   
		
        while(1)
        {
    
    
            printf("i am father pid=[%d], ppid=[%d]\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;                                                                                        
}

在这里插入图片描述
由于设置了waitpid(pid, &status, WNOHANG);在非阻塞情况下从结果可以看到父进程中的文件先打印,然后父子进程交替打印,在子进程循环10次之后,子进程退出,父进程继续运行,父进程来不及回收子进程,子进程也就变成了僵尸进程。如果要改变这种状态,将waitpid(pid, &status, WNOHANG); 改为while(waitpid(pid, &status, WNOHANG); == 0)即可改变子进程的僵尸状态。

4. 进程程序替换

进程程序替换是将已经跑起来的进程,替换成为执行其他程序的进程。

  1. 原理:将正在执行代码的进程当中的进程虚拟地址空间里面的数据段和代码段替换成为新的程序,当前在执行的进程就会执行替换之后的程序的代码。其中的堆栈也需要更新。
  2. 进程替换接口:execl函数簇,它不是一个函数,而是一对函数。
    2.1 execl函数原型:int execl(const char* path, const char* arg, …)
    2.2 参数:
      path:带路径的可执行程序,要替换的程序路径已经替换程序的名字;
      arg:给可执行程序传递的参数;注意传递的第一个参数必须是可执行程序的名称,后面依次传递参数,使用逗号隔开。如果已经写完了传递的参数,需要再传入一个NULL,表示已经传入完了。
      …:可变参数列表。
      规定:第一个传递的参数必须是可执行程序的名称,以NULL结尾,含义就是告诉execl函数参数传递完毕了
    2.3 返回值:只有替换失败之后才会有返回值,并且返回值小于0;如果替换成功,则没有返回值,并且执行新的程序,和之前的程序已经无关。
    举例如下:在这里插入图片描述
    在这里插入图片描述
    运行结果:由此可知替换成功。已经执行新的程序,和以前的程序无关了。
    加粗样式在这里插入图片描述
    3 int execlp(const char*, const char* arg, …);
    参数:
      file :待替换的可执行程序的名称,需要注意的是这个可执行程序必须在PATH环境变量中可以找到,待替换的可执行程序需要被操作系统找到,也可以传递带路径的可执行程序
      arg :给替换的可执行程序传递参数,相当于在命令行当中给程序传递命令行参数
       … : 可变参数列表,第一个传递的参数必须是可执行程序的名称,如果后面还有要传递的参数,使用逗号间隔一次传递,以NULL结尾,含义就是告诉execl 函数,参数传递完毕了。
  • 如果execl函数当中带有p, 则表示会搜索环境变量,并且第一个参数传入可执行程序的名称就可以,不带p,则表示不会搜索到环境变量,所以需要传递带路径的可执行程序。

4 int execle (const* path, const char* arg, … , char* const envp[])
参数:
   path:带路径的可执行程序
   arg:可变参数列表的给可执行程序传递的参数,以程序名称开始,以NULL结尾
  envp:程序员自己组织环境变量,如果不传入,则认为当前替换之后的程序没有环境变量

带e和不带e的区别
  如果不带e,则不需要程序员自己组织环境变量,如果带e,则需要程序员自己组织环境变量

函数名称当中带l和不带l的区别
  函数名称重带有l,表示参数是可变参数列表的形式

5 int execv(const char* path, char* const argv[])
参数:
  argv:传递给可执行程序的参数,第一个是可执行程序的名称,最后以NULL结尾

带v和不带v的区别
  如果带v,则表示参数是以字符指针数组的形式传递给exec函数的内容,进而传递给待替换的可执行程序。
几点说明

  • const int p —>const 修饰的是p ,*p不可改变,p指向了内存单元中的内容,const 修饰的是p,也就是指向内存单元当中的内容不能改变。
  • int const *p —>const修饰的是p, 指针p是不可以改变的,指向不可变,p当中保存的内存地址不可变,但内存当中存储的内容可变。-
  • const char* argv[]:不能改变argv数组中元素的内容
  • char* const argv[]:不能改变argv数组中元素的地址

6 int execvp(const char* file, char* const argv[])
  file: 可执行程序的名字
  argv:给可执行程序传递的参数

7 int execve(const char* filename, char* const argv[]. char* const envp[]);—>系统调用函数
  filename: 可执行程序的名字
  argv:给可执行程序传递的参数
  encp:程序员自己组织环境变量,如果不传入环境变量,则表示替换之后没有环境变量

进程程序替换最终图
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/aaaaauaan/article/details/107885967