【Linux】万字解读<进程控制>:创建&中止&等待&替换

2024-09-11 15:39:36 浏览数 (2)

前言 大家好吖,欢迎来到 YY 滴Linux系列 ,热烈欢迎! 本章主要内容面向接触过Linux的老铁 主要内容含:

一.进程创建

1.fork函数

【1】fork函数与其返回值
  • 它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
  • fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。
代码语言:javascript复制
#include <unistd.h>
pid_t fork(void);

返回值:在子进程中返回0
       在父进程中返回子进程id,出错返回-1
【2】fork函数中的写时拷贝
  • 通常,父子代码共享,父子再不写入时, 数据也是共享的
  • 当任意一方试图写入,便以写时拷贝的方式各自一份 副本 (蓝色部分) ,具体见下图:
  • 注意,其发生写时拷贝时,权限也会相应变化

2.fork函数/进程创建的场景——常规用法

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

二.进程终止

1.进程退出的场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

2.进程应对不同退出场景的退出方法

前置知识:echo $? 查看进程退出码
  • ?:保存的是最近一个子进程执行完毕时的退出码
  • 情况1:?= 0 ,表示成功
  • 情况2:?!=0 , 可以用1,2,3,4,5不同的数字表示不同的错误原因
代码语言:javascript复制
echo $?  ------指令
10 -------结果
前置知识:错误码VS退出码
  • **错误码:**通常是衡量一个库函数或者是一个系统调用一个 函数 的调用情况
  • **退出码:**通常是一个 进程 退出的时候,他的退出结果
  • **error错误码:**C语言提供的 全局变量 ,调用函数时如果出错了,他就会被设置成出错原因对应的数字
  • **strerror:**再把数字转换成描述出来的具体错误原因
代码语言:javascript复制
#include <errno.h>
-- 测试调用失败情况
FILE *fp = fopen("./log.txt","r");

strerror():
printf("after: %d, error string : %sn, errno, strerror(errno)");
【1】正常退出————通过退出码判断
1.exit函数 与 _exit函数
  • 任意地点调用exit,表示进程退出,不进行后续执行
  1. exit( 退出码 ) 是 库函数 ——头文件:stdlib.h
  2. _exit( 退出码 ) 是 系统调用 ——头文件:unistd.h
  • exit终止进程的时候, 会自动刷新缓冲区 exit终止进程的时候, 不会自动刷新缓冲区
验证是否自动刷新缓冲区
  • exit函数自动刷新缓冲区
代码语言:javascript复制
// 对比下面两个程序,一个带n,一个不带n
// n是行刷新,刷新到显示器上
int main()
{
    printf("you can see me!n"); 
    sleep(6);
    exit(1);
}现象:遇到刷新条件,立刻刷新显示you can see me,6s后,程序退出

int main()
{
    printf("you can see me!"); 
    sleep(6);
    exit(1);
}现象:不立刻显示,6s后程序退出,强制刷新显示
  • _exit函数 不会 自动刷新缓冲区
代码语言:javascript复制
// 对比下面两个程序,一个带n,一个不带n
// n是行刷新,刷新到显示器上
int main()
{
    printf("you can see me!n"); 
    sleep(6);
    _exit(1);
}现象:遇到刷新条件,立刻刷新显示you can see me,6s后,程序退出

int main()
{
    printf("you can see me!"); 
    sleep(6);
    _exit(1);
}现象:不立刻显示,6s后程序退出,也不会刷新显示
2.从main返回——return退出——等同于正常退出的exit
  • return是一种更常见的退出进程方法
  • 执行return n 等同于 执行exit(n)
  • 因为调用main的运行时,函数会将 main函数 的返回值当做 exit 的参数。
  • 换句话说, main函数 的退出码是可以被父进程获取的,用来判断子进程的运行结果
【2】异常退出(程序崩溃)——操作系统转换成信号——进程被操作系统杀掉
  • 我们输入kill -l 可以看到有许多信号
  • 崩溃时就是执行了-9信号,有时也会显示其他信号
  • 信号具体内容可以看YY后续信号博客
※ctrl c也能强制退出——信号终止

三.进程等待

1.进程等待基本介绍

  • 通过wait/waitpid的方式,让父进程(一般)对子进程进行 资源回收 的等待过程

2.进程等待的必要性

  1. 子进程退出,父进程如果不管不顾,就可能造成 ‘僵尸进程’的问题,进而造成内存泄漏——进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 也无能为力 ,因为谁也没有办法杀死一个已经死去的进程。
  2. 我们需要知道父进程派给子进程的 任务完成的如何 ,如:子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过 进程等待 的方式,回收子进程资源,获取子进程退出信息
  1. 有时候进程也会 等待硬件资源 ,利用wait进程等待把自己挂起

3.如何进行等待(wait&waitpid)

【1】wait函数参数与返回值介绍&演示
  • 返回值pid_t:成功返回 被等待进程pid ,失败返回-1。
  • 参数status: 输出型参数 ,获取子进程退出状态, 不关心则可以设置成为NULL
代码语言:javascript复制
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

演示:

  • 下方有一段程序,我们将参数设置成NULL即可
代码语言:javascript复制
void Worker(int number)
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %dn", getpid(), getppid(), cnt--);//蓝色部分
        sleep(1);
    }
}
int main()
{
    pid_t id = fork();
    if(id == 0) {//child
        Worker();
        exit(0);
    }
    else{//father橙色部分
       pid_t rid2 = wait(NULL);//wait函数
       if(rid == id)
       {
           printf("wait success, pid:%dn",getpid());
       }
    return 0;
}
  • 执行结果如下
  • 逐步打印完蓝色部分后(子进程),执行橙色部分(父进程)
  • 本质:子进程和父进程同时存在——>子进程变成 僵尸状态 ——>等待结束时, 子进程消失 ,只剩下父进程
【2】waitpid函数参数与返回值介绍&演示

返回值:

  1. 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
  2. 如果option设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  3. 如果调用中出错,则返回-1 ,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid: Pid=-1, 等待任一一个子进程 。与wait等效。 Pid>0.等待其 进程ID与pid相等 的子进程。
  • status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) 无需其他操作,设置成NULL即可
  • options:
  1. WNOHANG: 非阻塞等待。若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
  2. option设置为0,表示阻塞等待
代码语言:javascript复制
pid_ t waitpid(pid_t pid, int *status, int options);

演示:

  • 下方有一段程序,我们将参数中,pid设置成fork出的子进程id,status设置成NULL,而option设置成0——表示阻塞就等待的设置
代码语言:javascript复制
void Worker(int number)
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %dn", getpid(), getppid(), cnt--);
        sleep(1);
    }
}
int main()
{
    pid_t id = fork();
    if(id == 0)
    {//child
        Worker();
        exit(0);
    }
    else{//father
       printf("wait beforen");
       pid_t rid = waitpid(id,NULL,0);//waitpid函数
       printf("wait aftern");
       if(rid == id)
       {
           printf("wait success, pid:%dn",getpid());
       }
    return 0;
}
  • 执行结果如下
  • 先执行橙色部分(父进程)中的打印, 逐步打印完蓝色部分后(子进程) 【先后顺序与操作系统调度有关,不确定谁先开始,但一般是父进程最后退出】
  • 本质:子进程和父进程同时存在——>子进程变成 僵尸状态 ——>等待结束时, 子进程消失 ,只剩下父进程
【3】输出型参数-status的原理演示&status规则&现象演示(要点)
代码语言:javascript复制
pid_ t waitpid(pid_t pid, int *status, int options);
  • status参数 整形指针 类型,是 输出型参数 —— status用于接受子进程的退出码
  • 因为进程具有独立性,父进程无法直接获得子进程的退出信息
  • 我们也可以打印status观察它的值,但是要 注意status的规则 ! —— 意味着不能对status整体使用

原理:

  1. 我们需要定义一个整型变量 status
  2. 把 status 的地址,即我们的参数给内核;内核再 把这个地址指向区域上的值给回用户层变量中

status规则:

  • status是一个int的整数,一共32bit,只研究低16位
  • status用于接受子进程的退出码
  • 所以一共记录三种信息,1.退出状态 2.core dump标志 3.终止信号
  • 但是并 不是每个信息都会用到 下图分别是穷举2个场景的例子——正常终止以及被信号所杀时的区域划分情况

现象演示:

  • 把子进程退出码改成10exit(10);,通过status把子进程的退出信息返回
代码语言:javascript复制
void Worker(int number)
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %dn", getpid(), getppid(), cnt--);
        sleep(1);
    }
}
int main()
{
    pid_t id = fork();
    if(id == 0)
    {//child
        Worker();
        exit(10);//设置成10
    }
    else{//father
       printf("wait beforen");
       
       int status =0; //定义一个整型变量 status
       pid_t rid = waitpid(id,&status,0);//通过status把子进程的退出信息返回
       printf("wait beforen");
       if(rid == id)
       {
           printf("wait success, pid:%dn",getpid(),status);//打印status指向的区域的内容
       }
    return 0;
}
  • 执行结果为
代码语言:javascript复制
2560  //输出结果为2560,而不是10
  • 分析:该情况为正常退出,10作为退出状态填到9-15位的区域中,而打印是打印整体,也就是2560

四.进程程序替换

1.进程替换概念

【1】进程替换概念
  • 我们所创建的所有的子进程,执行的代码,都是父进程代码的一部分
  • 如果我们想让子进程 执行新的程序呢???-----进程替换:执行全新的代码和访问全新的数据,不再和父进程有瓜葛
  • 注意:进程替换不创建子进程—— 目标程序的进程不变pid不变,只是改变代码和数据
  • 如果直接替换代码和数据区,耦合可能同时影响父子进程,要维持进程的独立性——写时拷贝解决;我们有结论: 程序替换也存在写时拷贝
【2】进程替换的机制&情景演示

机制:

  • 进程替换成功: 子进程执行新的程序了, 剩下的代码不会执行了,被覆盖了。
  • 进程替换失败: 继续执行子进程剩余内容。 只有失败才有返回值

演示:

  • 当程序替换成功时,打印语句begin和执行替换后的语句,不打印语句end
  • 当程序替换失败时,打印语句begin和语句end
代码语言:javascript复制
//注:我们只要知道下面execl函数是 起到进程替换的作用就行
//即执行ls -a -l -n 指令,具体使用细节看下文exec类函数模块
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("pid: %d, exec command beginn",getpid());//语句begin
        execl("/usr/bin/ls", "lsaaa", "-a", "-l", "-n", NULL);
        printf("pid: %d, exec command endn", getpid());//语句end
        exit(1);
    }
    else{
        // father
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success, rid: %dn", rid);
        }
    }
    return 0;
}

2.进程替换用到的exec类函数解释&命名规则&使用演示

【1】exec类函数

exec类函数有如下几种:都是为了满足各种调用的场景

  • 通过man exec可查看

注意事项:

  1. 所有的exec类函数以 NULL结尾 表示完成
  2. 参数中后的…表示 可变参数
  3. exec用于程序替换,使用要满足:(1)必须找到可执行程序(2)必须要告诉exec*,怎么执行 ,下面会有具体演示
【2】exec类函数命名规则
  • l(list) : 表示参数采用 列表——execl ,execlp,execle
  • v(vector) : 参数用 数组——execv,execvp,execve
  • p(path) : 有p 自动搜索环境变量PATH——execvp
  • e(env) : 表示自己维护 环境变量——execve
【3】exec类函数使用演示
  • exec用于程序替换,使用要满足:(1)必须找到可执行程序(2)必须要告诉exec*,怎么执行 ,下面会有具体演示
1. execl
代码语言:javascript复制
#include <unistd.h>
int main()
{
 //第一个参数找到可执行程序
 //后面的参数以列表形式(带l)告诉exec怎么执行
 //以NULL结尾
 execl("/bin/ps", "ps", "-ef", NULL);
 execl("/usr/bin/top", "top", NULL);
 execl("/usr/bin/pwd", "pwd", NULL);
 execl("/usr/bin/bash", "bash","test.sh", NULL);
 exit(0);
 }
2. execlp
代码语言:javascript复制
#include <unistd.h>
int main()
{
 //第一个参数,execlp带p的,可以使用环境变量PATH,无需写全路径
 //后面的参数以列表形式(带l)告诉exec怎么执行 
 //以NULL结尾
 execlp("ps", "ps", "-ef", NULL);
 execlp("ls", "ls", "-a","-l", NULL);
 exit(0);
}
3. execv和execvp
代码语言:javascript复制
#include <unistd.h>
int main()
{
 //第一个参数,execv和execvp带v的,使用数组传参
 //后面的参数以列表形式(带l)告诉exec怎么执行 
 //以NULL结尾
 char *const argv[] = {"ps", "-ef", NULL};
 execv("/bin/ps", argv);
 
 // 带p的,可以使用环境变量PATH,无需写全路径
 execvp("ps", argv);
 exit(0);
}
【4】exec类函数原理
  • 事实上, 只有execve是真正的系统调用 ,其它五个函数最终都调用 execve
  • 所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示

0 人点赞