【Linux】【进程控制】

目录

一、创建进程

简单使用一下fork 

问:fork创建子进程,操作系统都做了什么?

写时拷贝

fork常规的用法

fork失败的原因

二、进程终止

1.进程终止时,操作系统做了什么?

2.进程终止的常见方式

获取最近一次进程退出的退出码 

strerror

代码没跑完,程序崩溃的情况 

3.用代码,如何终止一个进程

①main函数中的return语句

②exit退出进程

③_exit

库函数vs系统接口

三、等待进程

为什么要进行进程等待?

如何等待和等待是什么? 

1.wait-基本验证,回收僵尸进程的问题

wait的返回值

2.waitpid-获取子进程退出结果的问题

父进程获取子进程的status

进程异常退出,或者崩溃的本质

1.除以0来生成一个异常信号

2.野指针错误

3.死循环

进程的细节问题


一、创建进程

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

新进程为子进程,而原进程为父进程

【Linux】【进程控制】_第1张图片

简单使用一下fork 

 使用项目自动化构建工具创建我们的项目

【Linux】【进程控制】_第2张图片

这是写在myproc.c中的代码 

int main()
{
    printf("我是父进程\n");

    pid_t id = fork();
    if(id < 0)
    {
        printf("创建子进程失败\n");
        return 1;
    }
    else if(id == 0)
    {
        // 子进程
        while(1)
        {
            printf("我是子进程: pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else{
        // 父进程
        while(1)
        {
            printf("我是父进程: pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;
}

 【Linux】【进程控制】_第3张图片

  在我们程序执行的时候再打开一个窗口

【Linux】【进程控制】_第4张图片

ps axj |grep myproc

将pid和ppid都打印出来 

 ps ajx |head -1 && ps axj | grep myproc

【Linux】【进程控制】_第5张图片

#include
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

问:fork创建子进程,操作系统都做了什么?

fork创建子进程,是不是系统里多了一个进程?是的!

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

 内核数据结构是从操作系统来的,进程代码和数据是从磁盘来的,也就是你的C/C++程序加载之后的结果!

进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程(开辟空间)
将父进程部分数据结构内容拷贝至子进程(赋值和初始化,子进程的相关数据是以父进程为模板的)
添加子进程到系统进程列表当中
fork返回,开始调度器调度 

创建子进程,给子进程分配对应的内核结构,必须子进程自己独有了,因为进程具有独立性!

理论上,子进程也要有自己的代码和数据!

可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据!

所以子进程只能使用父进程的代码和数据

代码:都是不可以被写的,只能被读取,所以父子共享,没有问题!

数据:可能被修改的,所以必须分开!

对于数据而言:

1.创建进程的时候,就直接拷贝分离。

        但是可能拷贝子进程根本就不会用到的数据空间,及时用到了,也可能只是读取!

int main()
{
    const char *str = "aaa";
    const char *str1 = "aaa";
    printf("%p\n", str);
    printf("%p\n", str1);
    return 0;
}

 【Linux】【进程控制】_第6张图片

 我们发现这两个字符串的地址其实是一样的。

编译器在编译程序的时候,尚且知道节省空间。因为在编译的之后这个“aaa”是不可以被修改的,所以为了节省空间,所以只保存一份。

所以我们的子进程也是一样,创建子进程,不需要将不会被访问的,或者只会读取的数据拷贝一份。

但是,还是必须要拷贝的(因为数据的独立性),什么样的数据值得拷贝呢?

将来会被父或子进程写入的数据!!

1.一般而言,即便是操作系统,也无法提前知道,哪些空间可能会被写入。 

2.即便是提前拷贝了,你会立马使用吗?

所以操作系统采用了写时拷贝技术,来进行将父子进程的数据进行分离

操作系统为何要使用写时拷贝的技术,对父子进程进行分离 

1.用的时候,再给你分配,是高效使用内存的一种表现

2.在代码执行前操作系统无法预知那些空间会被访问

string的深拷贝和浅拷贝的问题

浅拷贝就是两个指针指向同一块空间,如果其中一个指针修改了这块空间的数据,另一个指针访问的时候会发现这块空间已经被修改过了

深拷贝就是两个指针指向两块不同的空间,这两块空间的内容是一样的。

所以string的深拷贝和浅拷贝的问题其实和我们上面的写时拷贝的问题有点相似之处

 fork之后,父子进程代码共享(是after共享的,还是所有的?)

所有的代码都是共享的!

1.我们的代码在汇编之后会有很多行代码,而且诶行代码加载到内存之后,都会有对应的地址

2.因为进程随时可能被中断,(可能并没有被执行完),下次回来,还必须从之前的位置继续执行(不是从最开始的位置哦!),就要求CPU记随时记录下当前进程执行的位置。所以CPU内有对应的寄存器数据,用来记录当前进程的执行位置!(EIP,也就是PC指针(point code),我们在此将其称为程序计数器)

(PC指针中永远记录的是当前正在执行的代码的下一行代码的地址!)

CPU其实只会取指令,分析指令,执行指令,而eip就是相当于是CPU的小秘书的职责,eip指向哪里,我们的CPU就执行哪里的代码

寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的,也就是进程的上下文数据!创建进程时候,要不要给子进程?

虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码!!

CPU如何认识指令?

通过指令集。比方说你想和外国人沟通,你可能就需要会英语。CPU也是一样,需要会一些简单的指令集,也就像是我们所学的英语一样。

写时拷贝

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

写时拷贝本质上值一种延迟申请,只有当真正要用到的时候才会分配资源。

【Linux】【进程控制】_第7张图片

 ​​​​​​​【Linux】【进程控制】_第8张图片

 因为有写时拷贝技术的存在,所以父子进程得以彻底分开!

完成了进程独立性的技术保证!(写时拷贝的好处)

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

fork常规的用法

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

一般而言,父子进程在实际使用的时候上面时候与我们生活中也是一样。比方说一个父亲希望子女和子女继承自己的事业,也就是第一种情况,又或者是希望子女去试一试别的事业,也就是第二种情况

fork失败的原因

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

进程创建,本质上是会消耗内存的。如果消耗的内存超出了系统的资源上限,系统就会封杀进程。并且一个普通用户能创建的进程个数其实是有上限的,系统不会让你创建太多的进程

二、进程终止

1.进程终止时,操作系统做了什么?

当然要释放进程申请的相关内核数据结构和对应的数据和代码

释放掉的本质就是释放系统资源(最主要的就是内存)。

2.进程终止的常见方式

①代码运行完毕,结果正确
②代码运行完毕,结果不正确(代码当中的逻辑有问题,没有达到预期的效果)
③代码异常终止(代码中存在野指针,或者越界之类的等等的行为,导致了程序崩溃)

这里我们先重点研究前两种。我们以main函数的返回值作为我们研究的切入点。

1.我们发现我们在写c/c++的时候我们的main函数的返回值只一个整数

2.我们的main函数总是会return一个0

那么main函数返回值的意义是什么?或者是说main函数return 0的意义是什么,为什么总是0?return 1 2 或者别的值不可以吗?

使用项目自动化构建工具Makefile

【Linux】【进程控制】_第9张图片

 再在myproc.c文件中写入下面的代码

#include 
int main()
{
  printf("hello world\n");
  return 0;                                                                       
}  

 这个代码很简单,就是会打印出一个hello world,但是这个return 0到底是什么意思呢?

这个return 0其实并不是总是0,可以是其他值。

一般来使我们的main函数的返回值可以叫做进程的退出码

这个进程的退出码是什么意思呢?

这里我们将我们的代码修改一下,将其return变成10,然后打印其进程和父进程的pid

【Linux】【进程控制】_第10张图片

 【Linux】【进程控制】_第11张图片

最终给我们看淡ppid是7832,这个7832其实就是我们的bash,也就是我们的命令行解释器。

这个10就是我们的进程退出码,,表示我们的进程是正确还是不正确返回的,如果退出码为0,代码退出的结果是正确的。如果退出的结果码非0,就代表运行的结果是不正确

获取最近一次进程退出的退出码 

echo $?

【Linux】【进程控制】_第12张图片

这时,我们就获取到了之前的退出码10。

main函数的返回码是返回给上一级进程(比方说是父进程),用来评判该进程的执行结果用的,可以忽略。 

我是先写的代码,我怎么知道这个运行结果正不正确呢?

不要忘了,main函数实际上是一个函数,你自己写的代码实际上也是可以封装成一个函数。是可以传回对应的返回值来判断是否成功完成了当前的工作的。比方说求1-100的和,我们就可以通过返回值的形式来判断程序运行的结果是否正确,当然如果不关心的话,返回0也没有关系

【Linux】【进程控制】_第13张图片

 比方说上面的代码,就会通过ret,也就是返回值来判断我们当前的进程有没有成功运行

【Linux】【进程控制】_第14张图片

 我们查看到返回值是0,就是成功运行

总结:程序结果跑完运行正确还是不正确,你关心返回值的话,可以通过对返回值的判断来知道我们的程序运行结果是否正确。

但是对于main函数的返回值其实还有更加深刻的考量 

比方说,我们去参加一场考试,正常来说,我们的考试结果会有三种结果,第一种就是我们将考试考完了,考试成绩通过,第二种情况是我们考试考完了,但是考试成绩不通过,第三种情况就是我们考试中工作比被抓住了,我们的考试提前终止了,我们的考试成绩作废了。我们先看前两种情况。

我们拿着我们的考试100分的成绩告诉家长,家长一般不回去关心你为什么考了100分。但是你拿着你的20分的试卷告诉家长,他们一定会质问你怎么才考这么一点分。

也就是说一旦程序运行的结果是正确的,没有人会关心结果为什么是正确的。只有当程序运行的结果是不正确的时候,别人才会去关心这个运行结果为什么是不正确的。

非零的返回值有无数个,不同的非零值就可以表示不同的错误原因。我们不妨用1表示考试前没睡好,2表示考试时候拉肚子,3表示考试之前没有好好复习。

所以在我们的程序给出运行结果之后,结果不正确是,方便定位错误原因的细节

所以退出码的意义除了判断程序是否正确运行之外,还能够方便判断程序运行错误的原因!

strerror

但是对于我们,一名程序员,可能并不清楚退出码1,2,3,4之类的是什么意思,所以我们需要将我们的错误码挥着退出码转换为字符(具体出错提示)的方案。 

这时,我们就可以使用strerror

【Linux】【进程控制】_第15张图片

编写我们的测试代码 

 【Linux】【进程控制】_第16张图片

 运行我们的程序

这些系统提供的退出码在133的时候大概就结束了 

这就代码着我们可以通过这些对应的退出码来返回对应的错误信息

我们自己可以使用这些退出码和含义,但是,如果你想自己定义,也可以自己设计一套退出方案!

这里我们打开一个不存在文件,它范围No such file or directory

我们发现这就是我们退出码为2的退出码信息 

 【Linux】【进程控制】_第17张图片

 【Linux】【进程控制】_第18张图片

 但是我们输入kill -9 111111111,显然这个11111111是一个不存在的进程,所以会报错。【Linux】【进程控制】_第19张图片

 但是我们打印出返回码,我们所得到的却是1,这根我们之前的标号对应不上

【Linux】【进程控制】_第20张图片

 这是因为的kill命令其实是有自定义的退出码的。

我们也可以使用这些退出码,比方说打开文件失败,直接return 1,也就是operation not permitted

在Linux下成功跑出来的代码,返回值都是0

代码没跑完,程序崩溃的情况 

【Linux】【进程控制】_第21张图片

前三条代码运行出来了,但是后面的代码没有运行出来 

 【Linux】【进程控制】_第22张图片

 程序崩溃的时候,退出码没有意义!一般而言退出码对应的return语句,没有被执行!就像是你考试作弊了,那么这个成绩都没有任何意义了。

我们此时更想要知道程序为什么会崩溃。

3.用代码,如何终止一个进程

①main函数中的return语句

【Linux】【进程控制】_第23张图片

 【Linux】【进程控制】_第24张图片

 return语句,就是终止进程的!return 退出码就可以终止一个进程。

只有main函数内的return语句才能够表示进程退出

②exit退出进程

【Linux】【进程控制】_第25张图片

【Linux】【进程控制】_第26张图片

我们发现exit可以成功退出并且返回其退出的数据

那如果我们在函数中也写入exit呢?

【Linux】【进程控制】_第27张图片

这里我们惊奇地发现我们的退出码变成了222,也就是在我们的sum函数中的那个exit的退出值

【Linux】【进程控制】_第28张图片

 所以exit在任何地方被调用,都代表着直接终止进程

③_exit

将我们之前的exit全部都换成_exit 

【Linux】【进程控制】_第29张图片

我们发现运行的结果跟我们之前的exit是一样的。

【Linux】【进程控制】_第30张图片

这里我们输入下面的测试代码

 【Linux】【进程控制】_第31张图片

由于我们的you can see me后面并没有\n也就是并没有将我们的缓冲区内的字符马上刷新到我们的呢屏幕上,所以我们的you can see me 会在等待三秒过后才将我们的内容打印到屏幕上。

【Linux】【进程控制】_第32张图片

 也就是说我们的exit是会在退出之前将我们的缓冲区的内容刷新到我们的屏幕上的。

我们修改一下exit为_exit,再次测试我们的程序

【Linux】【进程控制】_第33张图片

我们发现我们缓冲区中的内容并没有被打印出来!

 【Linux】【进程控制】_第34张图片

说明_exit是一个系统调用,直接终止掉这个进程,并不去管这个缓冲区中的内容是否已经刷新到屏幕上的问题,但是我们的exit会在关闭进程之前执行用户定义的清理函数,冲刷缓冲,关闭流等概念,最后再退出进程。

【Linux】【进程控制】_第35张图片

 我们更推荐使用exit

库函数vs系统接口

【Linux】【进程控制】_第36张图片

  exit()是C语言提供给我们的,是一个库函数,而_exit()属于系统接口,但是exit()在底层其实调用的是_exit()。

printf -\n数据是保存在“缓冲区”中的

请问这个缓冲区在哪里,指的是什么?缓冲区是由谁维护的?

通过上面的实验验证,我们可以知道缓冲区一定不在操作系统内部!!!

因为如果是操作系统维护的,,缓冲区_exit()也能将缓冲区中的内容刷新到屏幕上!

所以是c语言的标准库帮助我们维护的。

(初始窥见缓冲区)

三、等待进程

为什么要进行进程等待?

1.子进程退出,父进程不管子进程,子进程就要处于僵尸状态--导致内存泄漏

(另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。)

2.父进程创建了子进程,是要让子进程办事儿的,那么子进程把任务完成的怎么样,父进程需要关心吗?如果需要,如何得知?如果不需要,该怎么处理呢?

子进程把任务完成总共有三种结果:1.代码跑完,结果正确2.代码跑完,结果不正确3.代码没有跑完,程序崩溃了。

那么父进程如何知道子进程完成任务的结果呢?就是通过等待来完成。

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

如何等待和等待是什么? 

1.wait-基本验证,回收僵尸进程的问题

#include 
#include 
#include 
#include  

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

        }
        //h直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
}

重新打开一个终端,输入下面的代码,监视我们刚刚的进程

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

 【Linux】【进程控制】_第37张图片

 【Linux】【进程控制】_第38张图片

 这时,我们不妨使用wait接口

我们使用man 2 wait来查看一下wait的用法

【Linux】【进程控制】_第39张图片

 调用一个进程,直到这个进程的状态发生变化,也就是父进程等待这个进程的死亡。

wait的返回值

【Linux】【进程控制】_第40张图片

成功了就返回子进程的pid,失败了就返回-1.

创建我们的测试代码

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


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

        }
        //直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        pid_t ret=wait(NULL);//阻塞式地等待!说白了,就是让父进程处于一种阻塞状态
        if(ret > 0)
        {
            // 0x7F -> 0000.000 111 1111
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
        printf("ok\n");
    }
}

【Linux】【进程控制】_第41张图片

【Linux】【进程控制】_第42张图片

 我们看到在子进程运行期间,父进程一直等待着子进程的死亡。

为了让我们的测试结果更加明显一点,我们不妨将父进程sleep的时间调整为7秒

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


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

        }
        //直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(7);
        pid_t ret=wait(NULL);//阻塞式地等待!说白了,就是让父进程处于一种阻塞状态
        if(ret > 0)
        {
            // 0x7F -> 0000.000 111 1111
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
        printf("ok\n");
    }
}

也就是说子进程跑5秒,父进程跑7秒,大概有两秒钟的时间内,子进程处于僵尸状态

从下面的测试结果汇总,我们看到最终结果5513也就是我们的子进程的pid等待成功,然后等待期间也是大概有两秒子进程处于僵尸状态

【Linux】【进程控制】_第43张图片

 【Linux】【进程控制】_第44张图片

2.waitpid-获取子进程退出结果的问题

【Linux】【进程控制】_第45张图片

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
        当正常返回的时候waitpid返回收集到的子进程的进程ID;
        如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
        如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

        返回值>0表示等待子进程成功,小于零表示等待自己成失败
参数:
pid:Pid=-1,等待任一个子进程。与wait等效。

         Pid>0,等待其进程ID与pid相等的子进程。
status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结        束,则返回该子进程的ID。 (默认为0,表示阻塞等待)

waitpid(pid,nul,0)等价于wait(NULL)

输入测试代码

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


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

        }
        //h直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        //阻塞式的等待
        pid_t ret=waitpid(id,NULL,0);
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
    }
}

【Linux】【进程控制】_第46张图片  

 【Linux】【进程控制】_第47张图片

父进程获取子进程的status

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


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

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d,status: %d",ret,status);
            sleep(1);
        }
    }
}

【Linux】【进程控制】_第48张图片

但是我们观察到我们的status并不是我们之前父进程传给子进程的105,而是26880 !!

 【Linux】【进程控制】_第49张图片

 所以父进程并没有成功拿到我们子进程的退出结果。

这里我们需要解释一下status的构成

虽然我们的status在函数参数上是一个整数,再参数上将我们整型变量的地址传递给我们的status,waitpid是我们操作系统的接口,所以它最终就可以把这个值拿出来,给我们的操作系统读取,但是status并不是按照整数来整体使用的

子进程运行完毕是有三种结果的①代码跑完,结果正确②代码跑完,结果不正确③代码没有跑完,程序崩溃,

所以我们的status并不是按照我们整数的形式来整体使用的!,而是按照比特位的方式,将32个比特位进行划分,我们只学习低16位。

【Linux】【进程控制】_第50张图片

那么我们如果想要得到子进程的退出结果,我们应该如何去得到呢?

我们再对我们的代码进行一点修改


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

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            printf("等待子进程成功, ret: %d,子进程退出码: %d",ret,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

这里我们成功查看到了105,也就是我们子进程的返回值。 

【Linux】【进程控制】_第51张图片

进程异常退出,或者崩溃的本质

只要程序跑起来,它就是进程,和我们的语言的特性没有任何关系,而是属于操作系统的范畴。所以进程异常退出的本质是操作系统杀掉了我们的进程!

操作系统是如何杀掉我的进程的?本质上是通过发送信号的方式!

【Linux】【进程控制】_第52张图片

所以其实是操作系统给我们的进程发信号,将我们的进程杀掉的。每一个信号都是有编号的。(上面的编号看起来有64个,但是气势上是没有32和33号的)

前面31个是普通信号。

所以如果我们的进程如果因为野指针退出了,我们的进程一定会收到上面的前31个信号中的某一个,这31个信号我们就用status的低7个信号的比特位来表示

【Linux】【进程控制】_第53张图片 

之后我们用jdb调试崩溃程序的信号的时候,还会使用到core dump标志

注意,信号从1-31,是没有0号信号的!!

接下来我们编写代码查看我们上面的进程的信号,也就是status的低7位的数据

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


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

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

这里咱们的信号编号是0,代表我们的程序是正常退出的,运行期间没有错误。 【Linux】【进程控制】_第54张图片

 【Linux】【进程控制】_第55张图片

我们不妨制造一个异常信号查看一下会发生什么情况

1.除以0来生成一个异常信号

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            //产生一个异常信号
            int a=10;
            a/=0;
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

 

2.野指针错误

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            //产生一个异常信号
            int *p=NULL;
            *p=100;
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

 

【Linux】【进程控制】_第56张图片 

3.死循环

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        //死循环
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

当我们子进程运行起来的时候,在通过另外一个终端将我们的子进程杀掉,就能够看到死循环的返回信号。

 

【Linux】【进程控制】_第57张图片 

总结:程序异常不光光是内部代码有问题,也可能是外力直接杀掉(子进程代码跑完了吗?不确定,所以子进程的退出码没有任何意义)

进程的细节问题

1.父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid函数呢??直接全局变量不行吗?

 我们不妨使用代码直接验证一下

#include 
#include 
#include 
#include 
//从这两个导入的库可以看出这可能是一个系统接口
#include 
#include 

int code=0;
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        //死循环
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
        //设置退出码为105
        code=15;
        exit(15);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            printf("code: %d\n",code);
            sleep(1);
        }
    }
}

从我们的测试结果来看,我们的父进程根本拿不到这个全局变量的值 

【Linux】【进程控制】_第58张图片 

 进程具有独立性,那么数据就要发生写时拷贝,父进程无法拿到,更何况,信号呢?

2.既然进程是具有独立性的,进程退出码,不也是进程的数据吗?父进程又凭什么拿到呢??wait/waitpid究竟干了什么呢?

僵尸进程:至少要保留该进程的pcb信息!task_struct里面保留了任何进程退出时的退出结果信息!!

wait/waitpid本质上是读取子进程的task_struct结构,也就是说一个进程在退出的时候,会将自身的一些信号参数(退出码和退出信号)写入自身的pcb中,等待别的进程来读取这些信号。

3.wait/waitpid有这个权限吗?task_struct是内核的数据结构呀

当然有这个权限呀,因为wait/waitpid其实就是系统调用,不就是操作系统。

所以父进程没有权利直接去获得子进程的相关信息,但是父进程可以通过系统调用去获取子进程的退出结果

你可能感兴趣的:(Linux,linux,运维,服务器)