• Linux进程控制


    1. fork

    进程调用fork,当控制转移到内核中的fork代码后,内核做 :

    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表(运行队列)当中
    • fork返回,开始调度器调度

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

    因为fork之前只有父进程,fork之后有两个进程!

    那么fork之后是否只有fork之后的代码是被父子进程共享的??

    我们之前认识到进程具有独立性:代码和数据必须是独立的!我们针对数据给了写时拷贝的方案,代码是只读的,所以只能共享。

    一般情况,fork之后父子共享所有的代码!!

    子进程执行的后续代码 != 共享的所有代码,只不过子进程只能从这里开始执行!!

    为什么?

    CPU有一个寄存器叫eip:程序计数器,保存当前正在执行指令的下一条指令!在某些教材也叫pc指针。eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码出开始执行啦!!

    image-20220828215616873

    但是不代表子进程不能从头开始,子进程可以修改eip的值从main函数开始,就能从头开始执行代码!

    image-20220828213738168

    2.fork之后OS做了什么?

    进程 = 内核的进程数据结构 + 进程的代码和数据

    通过创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进程共享或者独立!!

    3.写时拷贝

    通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图

    父进程创建子进程之后,父子都有虚拟地址空间和页表,只不过子进程的各种隐射关系全部继承自父进程,所以父子的代码是共享的!

    但是页表里面还会包含读写属性的设置,写时拷贝的底层实现其实就是在创建子进程之后,把父子进程的页表隐射关系全部设置为只读的。

    所以读取的时候就支持读,当你写入时(当然也会判断写入的内存区域是代码还是数据)。比如说子进程想写入一份数据,父进程没有写入,所以父进程不管,依旧指向自己的代码和数据。而子进程代码是共享的,所以和父进程指向的代码区域是一样的!因为子进程想写入数据,所以操作系统会把数据拷贝到另一个区域,在此区域修改!有一个细节页表数据的只读属性被去掉了变成了正常属性,最后操作系统再重新修复父子进程页表的映射关系!

    image-20220829091359681

    写时拷贝本身就是由OS的内存管理模块完成的!所以我们平常感知不到!

    4. 操作系统为什么要写时拷贝?

    我们知道进程总要保持它的独立性,那么创建子进程的时候就把数据分开不行吗?

    1. 父进程的数据,子进程不一定会用,即便使用,也不一定全部写入。—会有浪费空间的嫌疑
    2. 最理想的情况,只有会被父子修改的数据,在子进程创建的时候就进行分离拷贝,不需要修改的共享即可 — 但是从技术角度实现复杂,即使是const也只能检查直接被修改的数据,有可能存在间接被修改的数据。fork的成本就太高了!
    3. 如果fork的时候,就无脑拷贝数据给子进程,会增加fork的成本(内存和时间上)。

    所以最终采用写时拷贝:只会拷贝父子修改的,变相的,就是拷贝数据的最小成本,但是拷贝的成本依旧存在。但是写时拷贝是一种延迟拷贝策略!只有真正使用的时候才给你!你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人!

    5.fork常规用法

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

    6. fork调用失败的原因

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

    7.进程终止

    7.1 关于终止的正确认识

    C/C++的时候,main是程序的入口函数,且通常最后我们都要写一句return 0.

    1. return 0,给谁return?
    2. 为什么是0?其他值可以吗?

    常见进程退出:

    1. 代码跑完,结果正确
    2. 代码跑完,结果不正确
    3. 代码没跑完,程序异常了

    所以进程代码跑完,结果是否正确:

    0:表示success,非0:表示失败。最想知道的是失败的原因!所以:非0标识不同的原因!return x其实是进程退出码!表征进程退出的信息,让父进程读取的,以便对处于僵尸态的子进程进行处理!

    证明:

    echo $?
    
    • 1

    这条命令表示在bash中最近一次执行完毕时对应进程的退出码!

    假设我们自己的程序里返回123,第一次就是123,第二次是0的原因是echo本身这个程序执行成功了!

    image-20220829102622275

    一般而言,失败的非零值我该如何设置呢?以及默认表达的含义?

    非零值可以自定义,错误码/退出码可以对应不同的错误原因,方便定位问题!

    7.2 关于终止的常见做法

    1. 在main函数中return,为什么其他函数不行呢?
    2. 在自己的任意代码任意地点中,调用exit(),括号里的就是退出码。
    3. _exit(),它和exit()很像,其实就是exit()调用了_exit()。

    exit和_exit的区别:
    exit:一秒之后数据就被刷新出来了,因为我们打印的时候没有加\n,数据还在缓冲区。程序退出后数据才被刷新出来。

    _exit:我们唯一做的就是把exit换成_exit.我们发现程序退出数据并没有被

    刷新出来。

    image-20220829104754884

    结论:

    exit终止进程,刷新缓冲区

    _exit直接终止进程,不会有任何刷新操作

    7.3 关于终止,内核做了什么

    进程 = 内核结构 + 进程代码 和 数据

    当一个进程退出时会先进入僵尸态,然后父进程会去等待它,回收子进程信息,说白了就是读取子进程的退出码!然后再将子进程设置为x状态,就可以是否子进程的内核结构,释放加载到内存的进程代码和数据。

    代码和数据必定会释放掉,内核结构(task_struct和mm_struct)操作系统可能不会释放。

    我们知道创建对象要先开辟空间,再初始化。废弃的内核结构的数据已经被释放了,但是内核结构开辟的空间还在,当你再次创建进程时,会把相应的task_struct和mm_struct重新初始化,就节省了重新开辟空间的消耗!

    内核的数据结构缓冲池,slab分派器。

    8. 进程等待

    8.1 为什么要等待?

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

    8.2 如何等待

    父进程调用wait就可以了,一个简单的样例:

    我们在40s内杀掉子进程,此时子进程就处于z状态,40s之后我们一旦等待成功了,子进程的z状态就没了。

    下面我们再写一段监控命令行脚本:

    while : ; do ps ajx | head -1 && ps ajx |grep mypro |grep -v 'grep\|worker\|master\|cache'; sleep 1; echo "################################################################"; done
    
    • 1

    image-20220829113842685

    运行结果:

    image-20220829120900639

    现在我们知道了,我们可以通过wait()的方案解决回收子进程z状态,让子进程进入x

    因为x太快了所以我们什么也看不到!

    8.3 waitpid

    #include 
    #include 
    
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int optinos);    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    pid_t:

    >0:等待子进程成功,返回值就是子进程的pid

    <0:等待失败

    pid:

    >0:是几就等待几号子进程,指定等待

    -1:等待任意进程

    options:

    0:阻塞等待

    *int status是一个输出型参数,wait/waitpid()是系统调用!!!通过调用该函数从函数内部拿出特定的数据,这里的内部是指从task_struct中拿出子进程退出的退出码!!

    image-20220829123321789

    8.4 int *status

    int *status这个整数其实是被当作位图在用,我们只关心它的低16位。我们通过这个整数的次低8位就能拿到子进程的退出码。1-7比特位可以拿到程序异常退出的终止信号。

    status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

    image-20220829123511463

    image-20220829125035116

    所以查看退出码还可以这样写:

    if(WIFEXITEL(status))
    {
        printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
    }
    
    • 1
    • 2
    • 3
    • 4

    8.5 问题1

    为什么不能定义一个全局变量code,子进程退出的时候就把code设置好特定的值,然后父进程回收的时候直接拿code的数据呢?

    绝对不可以,因为会发生写时拷贝。

    8.6 问题2

    一个子进程既有退出码:0,又有退出信号:11.

    那我先看谁?

    常见进程退出:

    1. 代码跑完,结果正确
    2. 代码跑完,结果不正确
    3. 代码没跑完,程序异常了

    退出码对应的是前两种情况,退出信号是第3种情况!

    程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!

    9. 阻塞等待和非阻塞等待

    如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。

    当我调用某些函数的时候,因为条件不就行,需要我们阻塞等待,本质:就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒!

    我们今天等待的资源就不是硬件了,而是软件。一个进程在等另一个进程!

    1. 代码没跑完,程序异常了

    退出码对应的是前两种情况,退出信号是第3种情况!

    程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!

  • 相关阅读:
    Docker 的数据管理和Dockerfile镜像的创建
    SpringBoot - @EnableGlobalMethodSecurity注解详解
    C++——智能指针
    持续测试下测试工程师的自我修养
    LLM文章阅读:Baichuan 2 干货
    期货开户几条建议帮助你
    使用Docker安装Drone和Gogs实现自动化部署
    虚拟内存相关笔记
    elasticsearch常用命令
    juc 之 一文看懂 CyclicBarrier、CountDownLatch、Semaphore的使用
  • 原文地址:https://blog.csdn.net/iwkxi/article/details/126593672