【Linux】-- 进程终止进程等待

目录

深入理解fork

进程终止

进程常见退出场景

退出码

总结

进程等待

进程等待必要性

wait与waitpid

阻塞等待

非阻塞等待

总结


深入理解fork

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

#问fork创建子进程的时候操作系统做了什么?

        fork创建子进程,等于系统中多了一个子进程。而进程 = 内核数据结构 + 进程代码和数据,内核数据结构来源与操作系统,进程代码和数据一般来源于磁盘(C/C++程序加载后的结果)。

        由于进程具有独立性,所以创建子进程需给子进程分配对应的内核结构。而对于一个进程来说,子进程应该也要有自己的代码和数据,但是对于fork之后创建的子进程并没有加载的过程,而是创建就立马运行。也就是说:子进程没有自己的代码和数据,子进程只能 “使用” 父进程的代码和数据。

融汇贯通的理解:

        fork创建子进程的特性是父进程的副本,父子进程代码共享。这与我们C语言中所学的常量字符串是类似的:
 

const char* a = "12345";
const char* b = "12345";

        对于相同的字符串常量,由于只能读不能写,在并不可能改变的情况下,采取两个空间存储一样的数据,无疑是对于空间的浪费。

        同样的道理,fork创建子进程的时候就直接进行代码和数据的拷贝分离,并不能保证子进程会的使用这些代码和数据,更或者用的到,也有可能只是读取。

新的问题在于是会有需要使用更改的地方:

  • 代码:都是运行即不可被写的,只能读取,所以父子共享没有问题。
  • 数据:可能被修改,所以必须分离

        由于操作系统无法知道:什么数据必须拷贝、什么数据值得拷贝、什么数据会被子或父进行写入。而且就算拷贝了,也不能保证数据会被立马使用。所以操作系统采用写时拷贝技术,进行对父子进程数据的分离

写时拷贝技术的意义:

  • 用的时候,再分配,高效使用内存。
  • 操作系统无法提前预知空间访问,采用立马用立马拷贝进行访问。

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


#问:fork之后,父子进程代码共享是所有,还是fork之后? 

         由于,我们的代码由编译器汇编之后,会有很多行代码。其会有自己的虚拟地址,也会有加载到内存中的物理地址。物理地址与虚拟地址会根据映射关系放在页表当中,虚拟地址会放在程序地址空间中,给CPU进行使用,CPU所能看见的仅仅是地址空间。也就是说CPU只知道虚拟地址,并不知道物理地址

        因为,进程可能随时在并未执行完的时候被中断。而下次回来执行,还必须从之前中断的位置继续执行。所以执行的位置需要CPU随时记录,所以CPU中会有对应的寄存器数据(EIP)来记录当前进程需执行的位置。

融汇贯通的理解:

        其实CPU也不是很聪明,甚至很笨。它只会执行:取指令、分析指令、执行指令(分析指令需要认识大量的指令。所以CUP很笨,但是不得不说它很强)

        而取指令就是CPU的等待任务安排,由寄存器中的上下文数据提供。

        CPU执行的内容靠EIP提供地址找到地址空间,再以虚拟地址通过页表映射找到物理内存中的数据,随后将数据给与CPU进行分析命令、执行命令。而EIP通过加减此次使用数据的大小到达下一个数据的位置。

        虽然父子进程各自调度,各自修改EIP,但是不重要,因为子进程认为EIP起始值就是需要执行的起始点(fork之后的代码)。所以,是共享所有

进程终止

#问:进程终止时,操作系统做了什么?
        进程终止时,要释放进程申请的相关内核数据结构和数据的代码。本质就是释放系统资源。

进程常见退出场景

#问:进程终止的常见方式?

  • 运行成功
    • 代码跑完,结果正确
    • 代码跑完,结果不正确
  • 运行失败
    • 代码没有跑完,程序崩溃了

退出码

#问:用代码,如何终结一个进程?什么是一个正确的终结?

  • 0:成功,正确。
  • 非0:标识的是运行的结果不正确。

融汇贯通的理解:

        在C/C++语言的书写上,对于main函数的return 0; 学语法的时候是说由于是int main(),返回值是int类型,所以需要返回一个整数(返回0就行了)。但是在学操作系统时,返回的是什么值就尤为重要了

        main函数内:return语句,就是终止进程的!(return 退出码)

写一个main函数return 0,并运行,可以发现:

命令:echo $?


最近(上一次)进程的退出码

        因为对于运行结果我们关心的永远是:它错了究竟错在哪里、而不是它对了究竟对在哪里。所以用无数的非0值标识不同错误的原因。给我们的程序在运行结束之后,对于结果不正确时,方便定位错误的原因细节。 

main函数返回

总体来说,mian函数返回值的意义是:返回给上一级进程,用来评判该进程执行的结果。

        我们可以利用一下代码将该进程的退出码打印出来。(每一个进程的错误码是不一定相同的,我们可以使用这些退出码和含义。但是。如果自己想定义,也可以自己设计一套退出方案。)

#include<stdio.h>
#include<string.h>
int main()
{
    for(int i = 0; i <= 134; ++i)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

        以上,就是main函数内的return语句,用于终止进程。并且return只有对于main函数来说是返回退出码。对于main内的函数,return语句用于跳出该函数,在需要时,也将函数返回值到调用表达式中。

exit函数与_exit函数

        exit在代码的任何地方都可以调用,都表示直接终止进程。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("你可以看见我吗?"); //注意此处没有写"\n"
    sleep(1);
    exit(100);
    return 0;
}

        _exit在代码的任何地方都可以调用,都表示直接终止进程。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("你可以看见我吗?");
    sleep(1);
    _exit(100);
    return 0;
}

        我们们可以发现一样的逻辑代码,只是使用了不同的exit函数与_exit函数,main函数确实在中途终止,并返回了我们随意写的退出码100。但是一个打印了语句,一个没有打印语句。这就是二者的区别。

知识回顾:

        在C语言中printf函数有一个特点,其需打印的数据是先放在缓冲区的,通过缓冲才打印出,日常我们所写的进程,是会结束时自动冲刷缓冲区的。而"\n"的是将存储在缓冲区的数据冲刷出,同时也打印后换行。

exit与_exit的区别

  • exit()是库函数(语言,应用)
  • _exit()是系统接口(操作系统)

        通过此我们也可以更近一步的知道。所谓的缓冲区一定不是由操作系统维护的,而是由C标准库维护的。因为如果是操作系统维护的,那么缓冲区_exit()也能进行刷新。

总结

        return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
        退出码是出现于代码跑完,运行成功的时候。而退出码是程序员对于自身代码逻辑的判断。所谓好的程序员是代码高内聚,低耦合、代码框架、对象描述是极简并清晰的。但是资深程序员,具备以上的同时,应该对于自身所写的代码逻辑是无比清晰的,可以预判到自己所写的代码可能会出现的错误,并用退出码标识出来。这样可以使得,其对于bug代码的处理无需寻找问题再处理,而是直接处理问题。这就是高效的编写代码。
        
        所以退出码是程序员对自身代码逻辑的一种错误预判标识。

进程等待

        利用wait与waitpid操作系统接口进行进程等待。

进程等待必要性

  1. 子进程退出,父进程不管子进程,子进程就要处于僵尸状态 -- 导致内存泄漏
  2. 父进程创建子进程,要是让子进程办事的,所以子进程任务完成的结果是重要的,父进程关心的。(需要结果,如何得知?不需要结果,如何处理?)
  3. 子进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

总体来说:为什么要进行进程等待?
        父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。父进程也可以不等待子进程,但是需要学习信号完才行。

融汇贯通的理解:
#问:什么是进程等待?

        答:通过系统接口,让用户等待子进程的一种方案(wait/waitpid)

#问:为什么进程等待?

        答:回收进程,父进程或者系统获取子进程的退出结果。


        父进程或者系统需要获取子进程的退出结果,进而就需要子进程维持住僵尸状态,读取结果。由于子进程结束成为并维持住了僵尸状态,就会出现僵尸进程的问题,所以需要通过进程等待的方式回收,否者就会导致内存泄漏。

        所以说,这是相互关联相互联系的,这也就是进程等待的作用。

        父进程需要获取子进程退出信息,而子进程任务完成的结果进程常见退出场景所说,分为:

  • 运行成功
    • 代码跑完,结果正确
    • 代码跑完,结果不正确
  • 运行失败
    • 代码没有跑完,程序崩溃了

wait与waitpid

命令:man 2 wait

命令:man 2 waitpid


查看wait与waitpid的英文文档

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

阻塞等待

wait方法

  • 返回值:
    • 成功返回被等待进程pid,失败返回-1
  • 参数:
    • status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL(不在wait讲解,在后面的waitpid讲解)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 2;
        while(cnt--)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        pid_t ret = wait(NULL); //阻塞式的等待!
    }
    return 0;
}

命令:while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "----------------------------"; done 


每个一秒循环打印一个表格的头标(表格中数据的名称),并查找里面的myproc并去除grep后的进程最后打印"----------------------------"(用于显示的分割)。

        父进程会一直处于阻塞等待,直到等待到子进程结束,父进程才执行后续任务。

融汇贯通的理解:

        阻塞的本质就是,当前进程调用某些接口,让自身处于某种等待资源条件的状态,当底层的条件没有就绪的时候,就要将自己处于某种等待队列当中,其中将自身的内核控制块当中的状态从R设置为S或者是D状态,处于等待某种资源的状态,当特定资源就绪时,就会立马从等待队列当中唤醒重新调用。

        简单来说就是:进程阻塞就是在系统函数内部,将进程放入阻塞队列当中。

        此处由于wait进入而阻塞等待,即内核数据结构从运行队列换出到某个阻塞队列中,等特定资源就绪就会换入到运行队列进行执行。

waitpid方法

  • 返回值:
    • 当正常返回的时候waitpid返回收集到的子进程的进程ID
    • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
  • 参数:
    • pid:pid=-1等待任一个子进程,与wait等效;pid>0等待其进程IDpid相等的子进程。
    • status:输出型参数,是一个32bit划分的参数(下面讲解)
    • options:默认为0:表示父进程阻塞等待;WNOHANG:表示父进程非阻塞等待。

        waitpid(pid, NULL, 0) == wait(NULL)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int code = 0;

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt--)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0; 

        // 只有子进程退出的时候,父进程才会waitpid函数,进行返回!![父进程依旧还活着呢!!]
        // waitpid/wait 可以在目前的情况下,让进程退出具有一定的顺序性!
        // 将来可以让父进程进行更多的收尾工作。
        pid_t ret = waitpid(id, &status, 0); //阻塞式的等待!
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d,子进程退出码: %d\n",\
                    ret, status & 0x7F ,(status >> 8)&0xFF); 
        }
    }
}

        输出型参数status并不是按照整数来整体使用的。而是按照比特位的方式,将32比特位进行划分,对于应用只需要学习低16位。

        次8位是进程的退出状态,低8位的第1位是core dump标志(gdb调试崩溃程序信号,此文用不到,先不讲解)。剩下的7位是终止信号。

其实waitpid的输出型参数status,对于退出状态、终止信号为了方便提取,都有其对应的提取方式:

  • WIFEXITED(status):提取退出状态。
  • WEXITSTATUS(status):提取终止信号。
if(WIFEXITED(status))
{
    //子进程是正常退出的
    printf("子进程执行完毕,子进程的退出码: %d\n", WEXITSTATUS(status));
}
else
{
    printf("子进程异常退出: %d\n", WIFEXITED(status));
}

对于进程结果的判断需要有先后:

  • 终止信号为0(运行成功),退出状态有效,可以看(结果是否正确)。
  • 终止信号为非0(运行失败),退出状态没有意义,不用看。

        进程信号:是进程是否成功运行完的关键。进程异常退出,或者崩溃,本质就是操作系统通过发送信号的方式杀掉了进程。

命令:kill -l


可以查看操作系统用于发送以杀死进程的终止信号(对于应用层,了解前31个即可)

向子进程内加入一个野指针:

int* i = NULL;
*i = 100;

        此时终止为非0,那么进程是被操作系统在中途杀死的,所以退出码没有意义。

外部杀死进程:

命令:kill -9 5748(子进程的pid)


将正在运行的子进程,通过命令行传入终止信号9杀死。

        程序异常,不光光是内部代码有问题,也有可能是外力直接杀死。

非阻塞等待

waitpid方法

        这就要使用waitpid中的参数options:WNOHANG选项,代表父进程非阻塞等待。

#问:为什么是WNOHANG而不是数字?

        在代码中没有具体含义的数字。叫做魔鬼数字/魔术数字,因为就一个数字放在那里,并无法直观的知道让他含义。所以采用WNOHANG的方式表明。

Linux是C语言写的 -> 系统调用接口 -> 操作系统提供的接口 -> 就是C语言写的 -> 系统一般提供的大写标记位,如:WNOHANG就是宏定义的:#define WNOHANG 1

便于的理解:

        WNOHANG可以理解为:Wait No HANG(夯),在编程上有一个口头说法:夯住了。也就是打开一个软件或者是APP系统没有任何的反应,也就是系统并未对该进程进行CPU调度。要么是在阻塞队列中,要么是等待被调度。

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include<vector>
#include<iostream>
#include <sys/types.h>
#include <sys/wait.h>

typedef void (*headler_t)();

std::vector<headler_t> handlers;

void fun_one()
{
    printf("这是一个临时任务1\n");
}

void fun_two()
{
    printf("这是一个临时任务2\n");
}

// 设置对应的方法回调
// 以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法!
void Load()
{
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt)
        {
            printf("我是子进程: %d\n",cnt--);
            sleep(1);
        }
    }
    else
    {
        int quit = 0;
        while(!quit)
        {
            int status = 0;
            pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待
            if(res > 0)
            {
                //等待操作成功 && 等待到子进程结束
                printf("等待等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status));
                quit = 1;
            }
            else if(res == 0)
            {
                //等待操作成功 && 等待到子进程结束
                printf("进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情\n");
                if(handlers.empty()) Load();
                for(auto iter : handlers)
                {
                    //执行处理其他任务
                    iter();
                }
            }
            else
            {
                //等待操作失败
                printf("wait失败!\n");
                quit = 1;
            }
            sleep(1); //每隔1s询问一次子进程是否结束
        }
    }
    return 0;
}

融汇贯通的理解:
#问:
为什么要用到wait/waitpid?

        答:进程具有独立性,数据就会发生写实拷贝,父进程无法拿到数据。需要wait/waitpid进行获取。

#问:wait/waitpid为什么能拿到子进程数据?

        答:子进程为提供给父进程提取数据,会维持僵尸进程。而僵尸进程保留了该进程的PCB信息,其中的task_struct里面保留了任何进程退出时的退出结果信息,wait/waitpid的本质就是读取进程task_struct结构。 

融汇贯通的理解:

  • 孤儿进程:

(子进程死亡晚于父进程)父进程结束了、死亡了。而子进程还在运行,此时子进程就会被1号init进程领养

  • 僵尸进程:

(子进程死亡早于父进程)父进程还在运行。而子进程结束了、死亡了,而子进程为了向父进程提供其需获取的数据,从而进入持续僵尸进程。但是父进程并未管他,进行回收,那么就会出现内核数据结构未释放,因为不会像new申请堆一样结束自动释放空间,于是导致内存泄漏。

  • 孤儿进程与僵尸进程的区别:

子进程死亡与父进程死亡的相对性早晚。

总结

等待主要就是阻塞等待与非阻塞等待。

  • 阻塞等待:一般都是在内核中阻塞,伴随着切换,等待被唤醒。

  • 非阻塞等待:父进程通过调用waitpid来进行等待,如果子进程没有退出,waitpid的这个系统调用会立马返回。

补充:

        我们需要明白,回收资源,获取子进程退出结果,不是我们做的,而是我们使用接口完成这个操作的。而接口是由操作系统做的,我们通过接口让操作系统去做,而且这个操作只能操作系统去做,因为操作系统是管理者。资源管理,资源调度……只有管理者才能调用。我们是处于最顶层

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川入

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值