【Linux】进程控制(创建, 终止, 等待, 程序替换)

目录

进程控制

进程创建

fork()

vfork()

fork 与 vfork的区别

进程终止

    进程终止的场景 : 

    进程终止有三种方式 :

    进程退出返回值

进程等待

为什么要进行进程等待?

进程等待的方法

获取进程退出返回值

程序替换

替换函数

具体替换 :


进程控制

    进程控制大概分为 : 进程创建, 进程退出, 进程等待, 程序替换

进程创建

    进程创建方式 : fork(), vfork().

fork()

1. 头文件 : #include

2. 返回值 : pid_t fork(void)   

    创建成功返回0, 失败返回-1, 父进程返回子进程的pid

创建子进程 : 

#include 
#include 
#include 
#include 

int main()
{
    pid_t pid = fork();
    if(pid< 0)
    {
        perror("fork error");
        return -1;
    }
    else if(pid == 0)
    {
        printf("i am child\n");
    }
    else
    {
        printf("i am parent\n");
    }
    return 0;
}

运行结果 : 

    我们可以看到, 父进程先运行, 然后子进程才运行,但其实我们用fork()创建出来的子进程和父进程谁先运行是不一定的

vfork()

1. 头文件 : #include

2. 返回值 : 成功返回0

我们先来看一段代码:

#include 
#include 
#include 
#include 

int main()
{
    pid_t pid = vfork();

    if(pid == 0)
    {
        printf("i am child ---- %d\n",getpid());
    }
    else
    {
        printf("i am parent ----- %d\n",getpid());
    }
    while(1)
    {
        printf("-------%d\n",getpid()); 
        sleep(1);
    }
    return 0;
}

运行 : 

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第1张图片

    我们可以看到,vfork()创建的子进程会在父进程之前运行, 这个是一定的, 只有当子进程exit()退出或者使用exec函数族程序替换的时候, 父进程才会开始运行, 我们将上面的代码加一句话 , 在子进程打印完了之后exit, 这个时候父进程就开始运行了: 

    if(pid == 0)
    {
        printf("i am child ---- %d\n",getpid());
        exit(0);
    }

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第2张图片

    但是不能在main函数中return退出, 这个时候子进程退出释放资源,包括虚拟地址空间里面存的东西,这个时候在centos系统下父进程会陷入一个调用栈混乱的情况, 有可能会重复运行, 那么在Debian系列比如ubantu系统下程序会崩溃,所以一定不要使用main函数中退出的方式退出子进程。

fork 与 vfork的区别

1. fork()创建出来的子进程和父进程谁先运行不一定

2. fork() 创建进程是将父进程的所有数据拷贝一份, 包括虚拟地址空间和页表, 这个时候他们两个里面所有的数据的虚拟地址都是一样的,但是当子进程对一个变量进行修改的时候, 这个时候系统会为这个 变量重新开辟空间, 也就是我们上篇博客中所说的 写时拷贝技术, 子进程与父进程代码共享, 数据独有

3. vfork()创建出来的子进程与父进程公用同一块虚拟地址空间, 这个时候我们在子进程中对数据进行拷贝的时候,父进程中会随着一起改变,有可能会造成函数调用栈混乱, 所以当fork实现了写时拷贝技术之后vfork基本就被淘汰了。

4. vfork存在的意义是快速创建子进程, 因为公用一块虚拟地址空间, 减少了子进程拷贝父进程的消耗, 所以速度快

5. vfork创建出子进程后一定是子进程先运行, 等到子进程exit退出或者exec函数族程序替换之后父进程才会开始运行。

 

进程终止

    进程终止的场景 : 

        1. 正常退出结果符合预期

        2. 正常退出结果不符合预期

        3. 异常退出

    进程终止有三种方式 :

        1. main函数中return 

        2. exit()        exit是库函数接口, 底层也是调用_exit,但是调用前会刷新缓冲区,做退出前的收尾工作

        3. _exit()      _exit是系统调用接口, 直接退出, 释放资源

1. return

#include 

int main()
{
    printf("nihao\n");
    return 0;
}

  调用return其实相当于调用exit(), 因为函数会将main的返回值当做exit的参数

2. exit()     

  头文件 : #include

#include 
#include 

int main()
{
    printf("nihao\n");
    exit(0);
}

3. _exit()

  头文件 : #include

#include 
#include 

int main()
{
    printf("nihao\n");
    _exit(0);
}

 

    进程退出返回值

    我们通过 : echo $?  查看进程正常退出的返回值

    比如: 

#include 
#include 

int main()
{
    printf("nihao\n");
    exit(255);
}

    获取返回值 :

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第3张图片

   

    这里我们还有一个概念叫做 : 错误编号

     系统调用完毕都会重置进程中errno这么一个全局变量,这个全局变量中存储的就是当次调用的系统调用接口错误编号,当系统调用接口出错, 用户就可以通过这个errno获取系统调用的错误原因

    比如 : 

#include 
#include 
#include 

int main()
{
    //先睡十秒
    sleep(10);
    printf("nihao\n");
    exit(255);
}

   我们Ctrl + c 结束终止进程, 然后获取退出返回值 :

      我们可以看到, 返回值并不是255, 而是130, 其实当程序异常退出的时候回返回一个未知数.

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第4张图片

   

 

进程等待

为什么要进行进程等待?

1. 我们之前在进程概念的章节说过, 如果不进行进程等待, 子进程退出父进程不知道, 会造成僵尸进程, 进而会内存泄漏

2. 僵尸进程无法杀死, kill -9 也不行

3. 父进程需要知道子进程的任务完成的如何, 以及结果是否正确, 然后回收子进程的资源, 回去子进程的退出信息

进程等待的方法

    wait 和 waitpid 

    具体的有阻塞和非阻塞两种 :

    阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 则一直等待, 直到完成后返回

    非阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 立即报错返回

1. wait

头文件 : 

    #include

    #include

函数 : 

    pid_t wait(int *status);

    返回值 : 成功返回子进程pid,  失败返回-1

    status : 子进程退出码, 输出型参数, 如果不关心子进程返回值可以置为NULL

注意

    wait等待子进程是一个阻塞等待, 死等, 如果子进程没有退出父进程不会运行.

比如 :

#include 
#include 
#include 
#include 

int main()
{
    printf("i am parent ---- %d\n",getpid());
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork error");
    }
    else if(pid == 0)
    {
        sleep(3);
        printf("i am child ---- %d\n",getpid());
        exit(0);
    }

    wait(NULL);
    while(1)
    {
        printf("i am ----%d\n",getpid());
        sleep(1);
    }
    return 0;
}

程序运行结果 :

    开始运行, 创建子进程, 3秒之后打印子进程信息, 子进程退出, 父进程等待子进程退出, 然后获取子进程退出返回值, 释放空间, 然后继续往下运行, 运行之前是一个阻塞等待.

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第5张图片

2. waitpid

头文件

     #include

函数

    pid_ t waitpid(pid_t pid, int *status, int options)

    返回值 : 正常返回的时候返回子进程的进程ID

        如果设置了选项 :  WNOHANG, 如果没有发现已经退出的子进程则返回0

        如果调用出错 : 返回-1, 这时error会被置成异常退出信号值

参数

    pid : 

        -1 : 等待任意一个子进程, 与wait等效.

        pid > 0 : 等待指定子进程

    status : 

        正常退出 : 正常退出返回值

        异常退出 : 异常退出信号值

    options : 

        0 : 阻塞等待

        WNOHANG : 将waitpid设置为非阻塞等待, 父进程一边干自己的事一遍等待, 如果有子进程退出则处理一下.

比如 : 

#include 
#include 
#include 
#include 

int main()
{
  int pid = fork();
  if(pid < 0)
  {
    perror("fork error");
    exit(-1);
  }
  else if(pid == 0)
  {
    sleep(5);
    exit(0);
  }

  int statu;
  int ret; 
  while((ret = waitpid(pid,&statu,WNOHANG)) == 0)
  {
    printf("打麻将\n");
    sleep(1);
  }
  printf("%d--%d\n",ret,pid);
  while(1)
  {
    printf("--------------------\n");
    sleep(1);
  }
  return 0;
}

运行结果 : 

     运行这个程序, 创建子进程并且让他睡5秒, 父进程进行一个waitpid的非阻塞等待, 子进程睡的5秒父进程一直在打麻将,  并且判断waitpid的返回值是不是0, 如果是0, 就是没有子进程退出, 如果不是0就是有子进程退出, 5秒之后子进程退出, waitpid的返回值也就是ret变成了子进程的pid, 退出循环, 获取子进程的退出返回值status, 然后释放资源, 父进程继续运行.

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第6张图片

获取进程退出返回值

1. wait 和 waitpid 都有一个参数 status 来获取进程的退出返回值

2. 如果传的是NULL, 那就是不关心返回值

3. status是一个4个字节的数据, 但是我们只关心它的低16位, 因为进程退出返回值是它的低16位的高8位

    我们来看一下status这个参数的构成 : 

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第7张图片

 获取子进程退出码 : 

        如果正常退出 : 低七位为0 , 退出码等于  (status >> 8) & 0xff

        如果异常退出 : 低七位不为0 , 没有必要获取返回值了,  会返回异常退出信号值

        判断是否正常退出 : status & 0x7f

           为0  : 正常退出, 获取返回值

           不为0 : 不获取返回值, 返回异常退出信号值

比如 : 

#include 
#include
#include 
#include 

int main()
{
    int pid = fork();
    if(pid < 0)
    {
        perror("fork error");
        exit(-1);
    }
    else if(pid == 0)
    {
        sleep(5);
        exit(255);
    }
    
    int statu;
    int ret;
    while((ret = waitpid(pid,&statu,WNOHANG)) == 0)
    {
        printf("打麻将\n");
        sleep(1);
    }

    //获取进程退出返回值
    if(!(statu & 0x7f))
    {
        //正常退出
        printf("%d---%d-----child exit code:%d\n",ret,pid,(statu >> 8) & 0xff); 
    }
    else
    {
        printf("%d---%d----exit:%d\n",ret,pid,statu & 0x7f);
    }
    while(1)
    {
        printf("--leihoua---\n");
        sleep(1);
    }
    return 0;
}

我们让它正常运行 : 

    可以看到, 成功获取到了子进程的返回值, 而且也拿到了子进程的进程ID

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第8张图片

我们在另一个终端中杀死子进程让它异常退出:

    我们可以看到这个时候进程退出的返回值不是255而是15, 其实这是异常的信号值, 现在我们先这样理解, 后面我们具体再说信号.

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第9张图片

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第10张图片

但是我们每次计算是否正常退出和退出的返回值太麻烦了, 所以在库里给了一套接口

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第11张图片

WIFEXITED(status) : 判断程序是否正常退出, 正常退出返回true.

WEXITSTATUS(status) : 获取正常退出返回值, 只有WIFEXITED返回true的时候才有用.

WIFSIGNALED(status) : 如果是否是被信号终止, 返回true.

WTERMSIG(status) : 获取异常退出信号值.

使用如下 : 

#include 
#include
#include 
#include 

int main()
{
    int pid = fork();
    if(pid < 0)
    {
        perror("fork error");
        exit(-1);
    }
    else if(pid == 0)
    {
        sleep(5);
        exit(255);
    }
   
    int statu;
    int ret;
    while((ret = waitpid(pid,&statu,WNOHANG)) == 0)
    {
        printf("打麻将\n");
        sleep(1);
    }

    //正常退出 
    if(WIFEXITED(statu))
    {
        printf("%d---%d-----child exit code:%d\n",ret,pid,WEXITSTATUS(statu)); 
    }
    if(WIFSIGNALED(statu))
    {
        printf("exit signal:%d\n",WTERMSIG(statu));
    }
    while(1)
    {
        printf("--leihoua---\n");
        sleep(1);
    }
    return 0;
}

运行结果如下 :

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第12张图片

 

程序替换

    用fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换函数

exec函数族 : 

【Linux】进程控制(创建, 终止, 等待, 程序替换)_第13张图片

    如果调用成功直接从新的程序启动代码开始执行, 没有返回值

    如果调用失败返回-1. 

 

exec函数族中 :

    有没有p的区别 : 是否自动到PATH所指定的路径下查找程序文件, 如果有就自动查找

    有没有e的区别 : 是否自定义环境变量, 如果有就自定义

    execl 和 execv 的区别 :

        l : 参数采用列表   list

        v : 参数采用数组, vector 

注意 : 

    这些函数中只有 execve 是系统调用接口, 其它的函数都是库接口, 最终其实还是要调用execve来实现

具体替换 :

    我们在这里给出几个简单的情况: 创建一个子进程去进行程序替换

#include 
#include 
#include 

int main()
{
    pid_t pid = fork();
    const char * envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    const char* ret[] = {"ls","-a",NULL};
    if(pid < 0)
    {
        perror("fork error");
    }
    else if(pid == 0 )
    {
        execlp("ls","ls","-a",NULL);
        execl("/bin/ls","ls","-a",NULL);
        execle("/bin/ls","ls","-a",NULL,envp);
        execv("/bin/ls",ret);
        execvp("ls",ret);
        execve("/bin/ls",ret,envp);
    }
    wait(NULL);
    return 0;
}

每一个语句运行的结果都是 :

 

以上就是关于进程控制的一些问题, 感谢观看

 

 

 

你可能感兴趣的:(Linux,Linux)