【Linux操作系统】简学深悟启示录:进程控制

文章目录

1.进程创建补充

关于进程创建及其本质都在前面的文章有详细介绍了,这里就不叙述太多,只进行细节补充

传送门:【Linux操作系统】简学深悟启示录:进程初步
【Linux操作系统】简学深悟启示录:环境变量&&进程地址

当子进程继承父进程的数据段的时候,无论该部分的权限之前如何,系统会将数据段的权限都设置成只读,当子进程需要修改共享数据时,此时会触发只读权限,系统不会将该修改识别为异常,而是自动修改权限并赋予子进程新的数据空间,实现写时拷贝

2.进程终止

进程退出的三种情况:

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

2.1 查看进程退出

bash 复制代码
[zzh_test@hcss-ecs-6aa4 PC]$ echo $?
0

比如最熟悉的 main 函数,最后总要加个 return 0; 表示进程退出状态正常,$? 查看的是最近一次执行进程的退出码

0 表示正常退出,除此以外用 strerror 解释退出码,可以看到多个退出原因

当进程异常时同样会返回退出码,但此时更重要的是异常的原因,即程序中断原因,应该查看信号,因为进程退出异常的本质就是收到了信号,这个后面会讲

2.2 exit 和 _exit

通过代码发现,exit 是用于返回进程码的函数,但是 exit 后的代码就再也没有执行了,这也就说明 exit 是进程退出,return 表示的是函数退出,二者不一样

我们知道对于 printf 来说 \n 的作用不仅是换行,更是起到刷新缓冲区让字符串强制输出的作用

去掉 \n,进行两种函数的对比,发现两种函数对于返回退出码的作用是一样的,但是一个输出了一个没输出,这是因为 _exit 是系统级别的调用,直接调用系统终止进程;exit 最终也是调用了 _exit 来终止进程的,但是还做了清理函数,刷新缓冲区的工作

3.进程等待

🤔为什么需要进程等待?什么是进程等待?

之前讲过如果子进程在父进程还在运行的时候进行了退出,父进程此时不对子进程进行处理,那么子进程会变成僵尸进程,此时连 kill -9 都无法杀死该进程,因为一个已经死掉的进程无法被杀掉,父进程派给子进程的任务完成的如何,我们需要知道,子进程运行完成,结果对还是不对,或者是否正常退出。因此父进程通过进程等待的方式,调用 wait / waitpid 回收子进程资源,获取子进程退出信息

3.1 wait 和 waitpid

c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    else if(id > 0)
    {
        int cnt = 10;
        while(cnt)
        {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = wait(NULL);
        if(ret == id)
        {
            printf("wait successful, ret: %d\n", ret);
        }
        sleep(5);
    }
    else
    {
        printf("fork fail\n");
    }
    return 0;
}

以上是一个使用 wait 回收子进程的例子

pid_t wait(int *status),该函数 status 用于获取子进程的退出状态,成功返回被等待进程 pid,失败返回 -1

  • 如果不关心子进程的退出状态,可传入 NULL ,就像代码中 pid_t ret = wait(NULL); 这种写法

  • 如果想获取子进程退出状态,可定义一个 int 类型变量,将其地址传入。通过一些宏来检查和解析状态信息,比如 WIFEXITED(status):判断子进程是否正常退出,若正常退出返回非零值,否则返回 0WEXITSTATUS(status):在 WIFEXITED 为真时使用,用于获取子进程通过 exitreturn 返回的退出码

c 复制代码
if (WIFEXITED(status)) 
{ // 判断子进程是否正常退出
    printf("子进程正常退出\n");
    printf("子进程的退出码是:%d\n", WEXITSTATUS(status)); // 获取子进程的退出码
} 
else 
{
    printf("子进程异常退出\n");
}

🔥值得注意的是: WIFEXITED 可以记忆为 wait if exited(等待是否退出),WEXITSTATUS 可以记忆为 wait exited status(等待退出状态)

通过执行代码,可以发现僵尸进程确实是被回收了,再深度思考,我们如何获取子进程的退出状态?比如异常了是被什么信号打断了?正常运行但是结果有错是什么原因造成的,此时处于什么状态?那么这个时候就用到了 waitpid 函数

c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    else if(id > 0)
    {
        int cnt = 10;
        while(cnt)
        {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        int status;
        pid_t ret = waitpid(id, &status, 0);
        if(ret == id)                                                                                                                                                           
        {
            printf("wait successful, ret: %d\n, status: %d", ret, status);
        }
        else
        {
        	printf("wait fail\n");
        }
    }
    else
    {
        printf("fork fail\n");
    }
    return 0;
}

要传入参数 &status,所以需要在外设置变量 status 获取子进程的退出状态(系统会自动捕捉子进程退出状态给到 status

观察 status 的值,256 不是正常的退出码值,为什么会出现这种情况?我们要知道,父进程等待子进程期望获得哪些信息?

  • 子进程代码是否异常
  • 没有结果,结果异常是为什么

所以 status 一定不是单纯的整数类型而已

对于 status 我们只看最低的 16 位,因为这里存储的是有效信息

7 位(第 1 - 7 位)为 0,表示是正常终止而非被信号杀死,第 8 位啥意思现在先不用管,高 8 位(第 8 - 16 位)存储 "退出状态"(即子进程通过 exitreturn 指定的退出码,比如 exit(1) 里的 1)。0000 00001 0000 0000 化为二进制是 0X7F,即 256,刚好就是打印出来的 status

✏️函数解析:

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

返回值:

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

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

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

参数:

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

3.2 阻塞和非阻塞轮询

如果子进程一直不结束,那么父进程 wait 岂不是要一直进行等待?确实是这样的,这种情况就是阻塞,那我们要如何优化呢?

waitpid 中将 option 参数设置成 WNOHANG 的方式就能避免阻塞的情况,他使用的是非阻塞轮询的方式

阻塞就是父进程一直等待子进程结束,因此会很耽误父进程自己的效率,非阻塞轮询不同的地方在于他是间歇性的询问子进程是否结束,如果没结束,父进程会继续干自己的事,比如打印日志等,就这样不断重复询问,直到子进程结束父进程就能回收了

4.进程替换

前面遇到的情况都是父子进程共用同一套代码,但是如果子进程想要实行另一套代码呢?那就需要用到进程替换了

4.1 进程替换本质

子进程进行进程替换时,数据代码父子共享,就有人会问了:那是不是就得创建新的进程来放新代码?还是进行写时拷贝?都不是!我们这里是整体替换,而不是部分修改,所以不涉及写时拷贝,更不涉及新进程创建

真正的方式: 清空子进程之前从父进程继承的内存映射(包括代码段、数据段等),从硬盘读取新程序,然后为子进程建立新的虚拟内存映射,并将文件内容加载到对应物理内存页中,该替换是由 exec 系列函数实现的

🔥值得注意的是:

  • 进程替换成功之后,exec* 后续的代码不会被执行,替换失败才可能执行后续代码,只有失败有返回值(比如 exit(1)),能看到 exit(1) 对应的返回值就表示进程替换失败了
  • 替换程序的时候,会将新程序的表头先加载进去,当真正要使用的时候才全部加载,这也是懒加载的一种表现
  • 环境变量默认不会被替换

4.2 进程替换函数

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        printf("替换前\n");
        execl("/usr/bin/ls", "-ls", "-a", "-l", NULL);
        printf("替换后\n");
        int cnt = 3;
        while (cnt)
        {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    else if (id > 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        int status;
        pid_t ret = waitpid(id, &status, 0);
        if (ret == id)
        {
            printf("wait successful, ret: %d, WIFEXITED: %d, WEXITSTATUS: %d\n", ret, WIFEXITED(status), WEXITSTATUS(status));
        }
        sleep(5);
    }
    else
    {
        printf("fork fail\n");
    }
    return 0;
}

在子进程进行主体函数运行时,添加一个 ececl 进程替换函数

根据结果,可以发现子进程程序替换之后的代码都没有执行

exec 开头的函数,这一系列的都是程序替换的函数,这些函数都封装了 execve 函数(系统调用函数)来间接调用

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

使用示例:

c 复制代码
char *argv[] = {"ls", "-a", "-l", NULL};
char *envp[] = {"VAR1=value1", "VAR2=value2", NULL};

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL);
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);


execv("/usr/bin/ls", argv);
execvp("ls", argv);
execve("/usr/bin/ls", argv, envp)

execlexecv 是一组的,第一个参数都是替换程序的绝对或相对路径,后面填的就是可变参数,指令怎么用这里就怎么填,记得要用 NULL 结尾,至于为什么,和命令行参数的道理是一样的,这两不同的就在于一个是直接传入,另一个是把参数放在数组里然后传入

传送门:命令行参数

execlpexecvp 是一组的,唯一的区别就是不用写路径,而是直接写要替换程序的文件名,前提是该文件名要在 PATH 环境变量下。execleexecve 是一组的,唯一的区别就是可以传入自己的环境变量,环境变量采用的是覆盖写入而不是追加

🔥值得注意的是:

引入 exec 系列函数时,通常会包括 extern char **environ,有人就问了,不是从父进程继承了吗?为什么还要写这条代码,确实你从父进程继承了该环境变量数据,但是你不知道在哪啊,需要外部声明来找到位置。补充一个知识点,除了可以 expot 写入 $PATH,还可以用 putenv()


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

相关推荐
wheeldown8 小时前
【Linux】为什么死循环卡不死 Linux?3 个核心逻辑看懂进程优先级与 CPU 调度密码
linux·运维·服务器·开发语言·c++·unix·进程
xxy.c8 小时前
嵌入式解谜日志-网络编程(udp,tcp,(while循环原理))
linux·运维·c语言·开发语言·数据结构
守.护10 小时前
云计算学习笔记——Linux系统网络配置与远程管理(ssh)篇
linux·运维·服务器·ssh·linux网络配置
津津有味道11 小时前
15693协议ICODE SLI 系列标签应用场景说明及读、写、密钥认证操作Qt c++源码,支持统信、麒麟等国产Linux系统
linux·c++·qt·icode·sli·15693
Lynnxiaowen12 小时前
今天我们继续学习shell编程语言的内容
linux·运维·学习·云计算·bash
Molesidy14 小时前
【随笔】【Debian】【ArchLinux】基于Debian和ArchLinux的ISO镜像和虚拟机VM的系统镜像获取安装
运维·debian·archlinux
喜欢你,还有大家15 小时前
Linux笔记14——shell编程基础-8
linux·前端·笔记
skywalk816315 小时前
mayfly-go:web 版 linux、数据库等管理平台
linux·运维·数据库
dbdr090115 小时前
Linux 入门到精通,真的不用背命令!零基础小白靠「场景化学习法」,3 个月拿下运维 offer,第二十四天
linux·运维·c语言·python·学习