【Linux】进程创建、进程终止、进程等待和进程程序替换

文章目录

  • 前言
  • [一. 进程创建](#一. 进程创建)
    • [1.1 fork 函数](#1.1 fork 函数)
    • [1.2 写时拷贝](#1.2 写时拷贝)
    • [1.3 fork 常规用法](#1.3 fork 常规用法)
    • [1.4 fork 调用失败的原因](#1.4 fork 调用失败的原因)
  • [二. 进程终止](#二. 进程终止)
  • [三. 进程等待](#三. 进程等待)
    • [3.1 进程等待的必要性](#3.1 进程等待的必要性)
    • [3.2 进程等待的方法](#3.2 进程等待的方法)
    • [3.3 获取子进程的status](#3.3 获取子进程的status)
  • [四. 进程程序替换](#四. 进程程序替换)
  • 最后

前言

在上一篇文章中,我们详细介绍了命令行参数、环境变量和进程地址空间的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解进程控制的内容,包括进程创建、进程终止、进程等待和进程程序替换,接下来一起看看吧!


一. 进程创建

1.1 fork 函数

在 Linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。


fork 函数一个系统调用,创建子进程成功时,返回0给子进程,返回子进程的pid给父进程;创建子进程失败则返回-1给父进程。

我们可以通过 fork 函数的返回值来对父子进程进行分流:

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id < 0){    
        // 创建子进程失败    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0){                                                                                                                                                               
        //子进程    
        printf("子进程, pid : %d\n",getpid());    
    }    
    else{    
        //父进程    
        printf("父进程, pid : %d\n",getpid());    
    }    
    
    return 0;    
} 

进程调用 fork ,当控制转移到操作系统内核中的 fork 代码后,操作系统内核做了什么?

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork函数返回,调度器开始调度

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意fork之后,谁先执行完全由调度器决定。

1.2 写时拷贝

当创建子进程时,父子进程的代码和数据共享,当父/子进程要进行数据修改时,操作系统就会进行写时拷贝,重新开辟一块空间给这个进程拷贝数据,然后修改它的页表(虚拟地址和物理地址)的映射关系,这样就完成了写时拷贝。

操作系统通过权限 来触发写时拷贝的,当创建子进程后,父和子进程的数据都改为只读 的,如果任意一个进程想要修改数据,就会报错,操作系统就会进行写时拷贝

写时拷拷贝是一种延时申请技术,可以提高整机内存的使用率

1.3 fork 常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如:子进程从 fork 返回后,调用 exec 函数(进程程序替换)。

1.4 fork 调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

二. 进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码

2.1 进程退出

当我们执行一个程序时,如果程序代码执行完了,那么它的运行结果可能符合我们的预期,也可能不符合我们的预期;程序也可能没有执行完,而是异常终止了。

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2 进程常见的退出方法

进程正常退出的场景有三种:

  1. main函数返回
  2. 调用exit函数退出
  3. 调用_exit退出

当我们使用Ctrl + C杀死一个进程属于进程的异常退出,通过信号让进程终止也属于异常退出。

exit函数

通过查看手册,可以发现exit函数属于3号手册,也就是C标准库的库函数;

那它的作用就是,退出一个进程并返回退出码;之前在C中执行的exit(1)就是退出程序并返回1

_exit系统调用

通过查看手册,我们发现_exit属于2号手册,也就是系统调用

它的作用也是退出进程,并且返回退出码。

exit_exit的区别

在Linux中,exit()_exit()都用于终止进程,但它们在行为细节 上有重要区别,主要体现在是否清理资源是否调用用户注册的退出处理函数


exit()标准库函数 ,会执行清理操作;
_exit()系统调用 ,直接让内核终止进程,不做任何清理

exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexiton_exit 定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

🔍 详细区别
特性 exit()(标准库函数) _exit()(系统调用)
是否刷新stdio缓冲区 ✅ 会刷新(如printf的输出) ❌ 不会刷新
是否调用atexit()注册的函数 ✅ 会调用 ❌ 不会调用
是否关闭打开的文件描述符 ✅ 会关闭(通过标准库) ❌ 不会关闭(由内核处理)
是否返回退出码给父进程 ✅ 会 ✅ 会
是否属于系统调用 ❌ 不是,封装了_exit() ✅ 是,直接调用内核

✅ 使用建议
场景 推荐
正常退出程序 exit()
子进程中避免刷新父进程缓冲区 _exit()(如fork()后子进程出错)
写底层系统程序或库 _exit() 避免副作用

exit函数

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
    
int main()    
{    
    printf("Hello Linux");    
    exit(1);                                                                            
    
    return 0;    
}


刷新缓冲区

_exit系统调用

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>                                                                      
                                        
int main()                              
{                                       
    printf("Hello Linux");              
    //exit(1);                          
    _exit(1);                           
                                        
    return 0;                           
}


不刷新缓冲区

return退出和exit退出的区别

  • return语言级 的函数返回,把控制权交还给上一层调用者;
  • exit库函数/系统级 的进程终止,把控制权直接交给操作系统

2.3 退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。

Linux Shell 中的主要退出码:

可以通过 echo $? 查看最近一次进程的退出码:

c 复制代码
#include <stdio.h>    
    
int main()    
{    
    printf("Hello Linux\n");                                                             
    return 1;    
}

通过echo $?命令可以查看最近程序的退出码为1,如果再执行一次echo $?命令可以看到输出的是1,因为echo也是一个程序,当成功执行时返回的就是0

注意当程序异常终止时,程序的退出码是没有意义的

三. 进程等待

3.1 进程等待的必要性

父进程必须等待子进程结束,内核才能彻底回收子进程的 PCB(进程控制块),否则子进程变成"僵尸",占用系统资源。

理由 后果
1. 回收退出码/状态 子进程的返回值、终止信号存在 PCB 里,只有父进程 wait 后内核才会释放这段内存。
2. 避免僵尸进程(Zombie) 子进程已死但 PCB 残留,PID 仍被占用;大量僵尸会耗尽系统 PID(pid_max),导致 fork 失败。
3. 向父进程传递 SIGCHLD 子进程终止时内核发送 SIGCHLD;父进程通常在该信号处理函数里调用 wait/waitpid 异步回收。
4. 同步父子生命周期 父进程可能要根据子进程是否成功、退出码是多少再做下一步动作;等待提供同步点。

总结 :父进程通过进程等待的方式,回收子进程资源获取子进程退出信息

3.2 进程等待的方法

可以通过系统调用waitwaitpid

wait方法

wait只有一个参数statusstatus是一个输出型参数,简单来说就是:我们想要让父进程获得子进程的退出信息,就传递一个int* 指针,这样在函数调用结束后,父进程就拿到了子进程的退出信息;(如果不需要获取子进程的退出信息,可以传NULL)。

如果等待成功则返回被等待进程pid,等待失败则返回-1

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>    
    
int main()    
{    
    int id = fork();    
    if(id < 0){    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0){    
        //子进程    
        printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());    
        sleep(3);    
        exit(1);    
    }    
    //父进程    
    sleep(5);    
    wait(NULL);    
    sleep(100);    
    
    return 0;    
}

waitpid方法

wait不同的是:waitpid多了两个参数:pidoption

参数

  1. pidpid=-1,等待任一个子进程,与wait等效;pid>0,等待其进程ID与pid相等的子进程。
  2. status:输出型参数。WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出);WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  3. options:默认为0,表示阻塞等待;WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

返回值

当正常返回的时候waitpid返回收集到的子进程的进程ID

如果设置了选项WNOHANG ,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

现在来模拟一下非阻塞等待:

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
    int id = fork();    
    if(id < 0)    
    {    
        perror("fork");    
        return 1;    
    }    
    if(id == 0){    
        //子进程    
        int cnt = 3;    
        while(cnt--)    
        {    
            printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());    
            sleep(1);    
        }    
        exit(1);    
    }    
    
    //父进程    
    printf("等 待 开 始\n");    
    while(waitpid(-1,NULL,WNOHANG) == 0)    
    {    
        printf("父进程: pid : %d, ppid: %d\n",getpid(), getppid());    
        sleep(1);    
    }    
    printf("等 待 结 束\n");    
    sleep(100);                                                                                                                                                                     
    return 0;    
}

我们使用非阻塞等待要轮循调用waitpid,再等待过程中,父进程可以执行自己的代码。

3.3 获取子进程的status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

只研究status低16比特位,高16比特位没有用,所以要把它看作一个位图。在低16比特位中的高8位表示进程退出时的退出码,低8位表示进程的退出信号(实际上是7位,有一位是core dump标志,后面会讲解)

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>                                                                                                                                                                 
    
int main()    
{    
    int id = fork();    
    if(id < 0){    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0){    
        //子进程    
        printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());    
        exit(1);    
    }    
    
    //父进程    
    int status = 0;    
    //阻塞等待    
    waitpid(-1,&status,0);    
    printf("child status : %d\n",status);    
    return 0;    
}

为什么退出码是256?而不是1?

因为退出码占8位,退出码为1,而退出信号占低8位,为全0,所以输出的是2的8次方,即256

获取退出码

只需让status>>8然后再按位与&0xFF(1111 1111)即可获得退出码

获得退出信号

退出信号实际上只占低7位,只需让status按位与&0x7F(0111 1111)即可获得退出信号

c 复制代码
#include <stdio.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>    
    
int main()    
{    
    int id = fork();    
    if(id < 0){    
        perror("fork");    
        return 1;    
    }    
    else if(id == 0){    
        //子进程    
        printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());    
        exit(1);    
    }    
    //父进程    
    int status = 0;    
    waitpid(-1,&status,0);    
    printf("child status : %d\n",status);    
    printf("status exit code : %d\n",(status>>8)&0xFF);                                                                                                                             
    printf("status exit signal : %d\n",status&0x7F);    
    return 0;    
}

操作系统中还存在一些宏,可以直接使用这些宏来获取退出码和退出信息:

退出码

  • WIFEXITED(status):判断程序是否正常退出;
  • 返回true就表示程序正常退出;
  • WEXITSTATUE(status):当进程正常退出时,可以通过WEXITSTATUE获取进程的退出码。

退出信号

  • WTERMSIG(status):当进程异常退出时,可以使用WTERMSIG获取当前进程的退出信号。
c 复制代码
#include <stdio.h>      
#include <stdlib.h>      
#include <sys/types.h>      
#include <sys/wait.h>      
#include <unistd.h>      
      
int main()      
{      
    int id = fork();      
    if(id < 0){      
        perror("fork");      
        return 1;      
    }      
    else if(id == 0){      
        //子进程      
        printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());      
        exit(1);      
    }      
    //父进程      
    int status = 0;      
    waitpid(-1,&status,0);      
    if(WIFEXITED(status)){      
        printf("status exit code : %d\n", WEXITSTATUS(status));                                                                                                                     
    }                                                                                                                                                              
    else{                                                                                                                                                          
        printf("status exit signal : %d\n", WTERMSIG(status));                                                                                                     
    }                                                                                                                                                              
    return 0;                                                                                                                                                      
}

四. 进程程序替换

fork() 之后,父子各自执行父进程代码的一部分,如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

4.1 替换原理

fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("进 程 切 换 开 始\n");    
    execl("/usr/bin/ls","ls","-a","-l",NULL);                                                                                                                                       
    printf("进 程 切 换 结 束\n");    
    return 0;    
}

我们可以看到进程的程序是替换了,为什么没有执行最后的printf语句(printf("进 程 切 换 结 束\n"); )呢?

因为程序一旦被替换,就不会再执行往后的代码了,进程的代码和数据都被替换成新的代码和数据了。

4.2 替换函数

其实有六种以 exec 开头的函数,统称exec函数:

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1
  • 所以 exec 函数只有出错的返回值而没有成功的返回值

命名理解

  • l (list):表示参数采用列表
  • v (vector):参数用数组
  • p (path):有p自动搜索环境变量PATH
  • e (env):表示自己维护环境变量

execl

execl 函数参数存在两个

  1. 第一个表示要执行新的程序所在的路径,执行系统指令时也要带上路径
  2. 第二个则是参数列表,表示要怎么执行这个新的程序;有l时表示以参数列表 的形式调用,参数列表要以NULL结尾。
c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("进 程 切 换 开 始\n");    
    execl("/usr/bin/ls","ls","-l",NULL);                                                                                                                                            
    printf("进 程 切 换 结 束\n");    
    return 0;    
}

执行自己写的程序:

code.c

c 复制代码
#include <stdio.h>    
    
int main(int argc, char* argv[])    
{    
    for(int i = 0;i < argc;i++)    
    {    
        printf("argv[%d] : %s\n", i, argv[i]);    
    }                                                                                                                                                                               
    
    return 0;    
}

test.c

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("进 程 切 换 开 始\n");    
    execl("./code","./code","-a", "-b", "-c",NULL);                                                                                                                                 
    printf("进 程 切 换 结 束\n");                                   
    return 0;                                                        
}

execlp

execlpexecl相比唯一的不同就是,执行系统指令时不需要带路径(它会通过PATH环境变量去寻找指定的系统指令)

当然使用execlp函数也可以执行自己写的程序,只不过要带上路径。

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    execlp("pwd","pwd",NULL);    
                                                                                                                                                                                    
    return 0;                         
}

execle

execle有三个参数:

  1. 新的程序所在的路径(系统指令也要带路径)
  2. 参数列表,以NULL结尾
  3. 环境变量表。

有时候我们可以传自己维护的环境变量表,而不用自带的。

code.c

c 复制代码
#include <stdio.h>    
    
int main(int argc, char* argv[], char* env[])    
{    
    for(int i = 0;i < argc;i++)    
    {    
        printf("argv[%d] : %s\n", i, argv[i]);    
    }    
    
    for(int i = 0; env[i]; i++)    
    {    
        printf("env[%d]: %s\n", i, env[i]);                                                                                                                                         
    }    
    return 0;    
}

test.c

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    extern char** environ;    
    execle("./code","./code", "-a", "-b",NULL, environ);                                                                                                                            
    return 0;             
}

execv

execv有两个参数

  1. 新的程序所在的路径(系统指令也要带路径)
  2. 命令行参数表,以数组的形式存储,最后一个存NULL
c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    char* const argv[] = {    
        (char* const)"ls",    
        (char* const)"-a",    
        (char* const)"-l",    
         NULL    
    };    
    execv("/usr/bin/ls",argv);    
                                                                                                                                                                                    
    return 0;    
}

execvp

execvpexecv的区别就是,在执行系统命令时,可以不带路径;execvp会在环境变量PATH中找到指定程序。

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    char* const argv[] = {    
        (char* const)"ls",    
        (char* const)"-a",    
        (char* const)"-l",    
         NULL    
    };    
    execvp("ls",argv);                                                                                                                                                              
                                                                                      
    return 0;                                                                         
}

execvpe

execvpe存在三个参数:fileargvenv

  • file:第一个参数,指的是新程序所在的路径。
  • argv:第二个参数指的是命令行参数表,以数组的形式存储。
  • env:第三个参数指的是环境变量表。

code.c

c 复制代码
#include <stdio.h>    
    
int main(int argc, char* argv[], char* env[])    
{    
    for(int i = 0;i < argc;i++)    
    {    
        printf("argv[%d] : %s\n", i, argv[i]);    
    }    
        
    printf("\n");    
                                                                                                                                                                                    
    for(int i = 0; env[i]; i++)                                                                                                 
    {                                                                                                                           
        printf("env[%d]: %s\n", i, env[i]);                                                                                     
    }                                                                                                                           
    return 0;                                                                                                                   
}

test.c

c 复制代码
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    char* const argv[] = {    
        (char* const)"./code",    
        (char* const)"-a",    
        (char* const)"-b",    
        (char* const)"-c",    
        NULL    
    };    
    //使用自己的环境变量表    
    char* const env[] = {    
        (char* const)"MYNAME=XZX",    
        (char* const)"MYVAL=666666",    
        NULL    
    };    
    execvpe("./code", argv, env);                                                                                                                                                   
    
    return 0;    
}

这里简单总结一下这些函数:

4.3 execve

与上面的exec系列的函数不同,execve是一个系统调用

简单来说上面的exec系列是库函数,而execve是操作系统提供的系统调用

exec系列函数对execve系统调用做了封装

  • 当使用execlp执行系统命令不带路径时,execlp会根据环境变量PATH找到对应程序的路径,然后传新程序的路径给execve来调用。
  • 当使用execlexecv等函数没有传环境变量表时,在调用execve系统调用时会传当前环境变量表environ
  • 当我们使用execl等带l的函数时,传递的参数列表都会被转化成参数数组 ,然后再将参数数组传递给execve

最后

本篇关于进程创建、进程终止、进程等待和进程程序替换的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

相关推荐
梦想的颜色2 小时前
阿里云ecs云服务器linux安装redis
linux·服务器·阿里云
Y淑滢潇潇2 小时前
RHCE Day5 SELinux
linux·运维·rhce
是垚不是土2 小时前
运维新人踩坑记录:Redis与MySQL生产故障排查&优化手册
运维·数据库·redis·mysql·云计算·bootstrap
snpgroupcn2 小时前
如何在SAP中实现数据验证自动化?5天缩短验证周期,提升转型效率的3大关键策略
运维·人工智能·自动化
optimistic_chen2 小时前
【Linux 系列】Linux 命令/快捷键详解
linux·运维·服务器·ubuntu·命令行·快捷键
ICT技术最前线2 小时前
如何高效测试Linux系统连通性?
linux·网络·智能路由器
浅笑离愁12343 小时前
VI视频输入模块学习
linux·音视频
gzr_csdn3 小时前
【报错解决】VMware 嵌套虚拟化问题
linux·容器
TH_13 小时前
腾讯云-(1)-轻量级服务器购买
服务器·云计算·腾讯云