[Linux 进程控制(二)] 写时拷贝 - 进程终止

在这里插入图片描述

文章目录

  • 1、写时拷贝
  • 2、进程终止
    • 2.1 进程退出场景
      • 2.1.1 退出码
      • 2.1.2 错误码
      • 错误码 vs 退出码
      • 2.1.3 代码异常终止引入
    • 2.2 进程常见退出方法
      • 2.2.1 exit函数
      • 2.2.2 _exit函数

本片我们主要来讲进程控制,讲之前我们先把写时拷贝理清,然后再开始讲进程控制。

1、写时拷贝

我们第一篇进程文章中,讲到了系统接口fork()创建子进程,最后我们提了五个问题,第五个问题:如何理解同一个id变量,怎么会有不同的值? 写时拷贝将为你解答该问题。记不清的伙伴点这里回顾那篇文章
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在这里插入图片描述
当父进程创建子进程之后,子进程的页表是拷贝父进程的,但子进程要在数据段进行写入(代码段不支持修改),就需要重新申请空间,将原数据拷贝后再做写入我们并不是将整块数据进行改写的,可能只是修改部分数据),并修改页表,这部分工作是由操作系统做的。 但是该工作是需要时机的,操作系统并不知道你什么时候是要做写入的。
我们先说一个**结论**:父进程创建子进程的时候,首先将自己的读写权限改为只读,然后再创建子进程。

用户是不知道的!用户将来可能会对数据(权限为读写,代码段是只读) 进行写入!此时,页表的转换会因为权限问题出错,这时操作系统就接入了。但是出错也分真假:

  • 真出错。代码段是不可以写入的,但是我们修改的区域在code_start~code_end(代码区起始结束区域),这时就是越界/真出错。
  • 假出错。对数据区的写入,数据区是可以读写的,只是我们页表中改成了只读。这样的不是出错,是触发进行重新申请内存,拷贝内容的策略机制。

我们终于明白了,子进程拷贝下父进程的页表后,将数据对应的页表条目权限改为只读,通过让操作系统触发异常的方式,让操作系统帮我们进行写时拷贝的,完成后再把对应的页表条目改为读写,没有写入的依旧是只读。

2、进程终止

我们先来提出一个 问题:我们C语言代码main函数最后都有一个return 0,返回0时给谁返回呢?
main函数也是被调用的,所以注定谁调用就给谁返回。我们写一段代码来看看:

#include <stdio.h>int main()
{return 10;
}

我们main函数中什么都不写,直接返回值为10。
当编译运行后,它的父进程是bash,会将返回值交给父进程,用指令echo $?获取刚刚的结果。
在这里插入图片描述

打印出来这是现象。
?是环境变量,保存的是最近一个子进程执行完毕的退出码。
在这里插入图片描述

第二次查看退出码为0,是因为上一个echo执行是成功的,0代表了成功。
由此我们展开下面的话题:

2.1 进程退出场景

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

2.1.1 退出码

在多进程环境中,我们创建子进程就是为了帮我们去做事,这里“我们”是父进程,子进程事做的怎么样,父进程是需要知道的。在main函数中,返回值0代表正确,非0代表错误,父进程就是依靠返回值来判断是否正确的做完了任务。
当返回0,正确大家不会关心这个过程;但是返回非0,意味着错误,我们最想知道的是错误的原因是什么。所以我们可以用不同的数字表示不同的原因!但是不便于人阅读,所以我们需要一些能够将数字转化成错误码的字符串描述方案。C语言给我们有提供一批接口,我们也可以自定义一批我们自己的错误码与错误信息,把不同数字转化成不同出错原因的接口:

在这里插入图片描述

我们写一段代码来打印一下所有的退出码与对应的信息:

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

在这里插入图片描述

这就是退出码,不同的退出码代表不同的出错原因。
我们来举例子看一下:
在这里插入图片描述

退出码是2,错误信息描述是没有这样的文件或目录。跟我们上面查看的退出码以及对应信息是匹配的。
结论main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果。

2.1.2 错误码

C语言还有一个错误码,errno,我们下面来学一下看看有什么不同:
我们先写一个代码测试一下:

#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{printf("before: %d\n", errno);FILE* fp = fopen("./log.txt", "r");printf("after: %d, error string: %s\n", errno, strerror(errno));return 0;
}

我们当前路径下是不存在log.txt文件的,以读的方式打开肯定是错误的,我们打开前输出一次,打开后输出一次。
在这里插入图片描述
这说明,错误码会在调用接口的时候被设置。

错误码 vs 退出码

  • 错误码通常是衡量一个库函数或者是一个系统调用(Linux内核也是用C语言写的,所以它也可以访问errno)函数的调用情况。
  • 退出码通常是一个进程退出的时候,他的退出结果。
  • 相同点:当失败时,用来衡量 函数/进程 出错时的出错详细原因。

当我们写的代码有多个系统接口和库函数,我们可以把退出码和错误码设置成一致:

#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{int ret = 0;printf("before: %d\n", errno);FILE* fp = fopen("./log.txt", "r");if(NULL == fp){printf("after: %d, error string: %s\n", errno, strerror(errno));ret = errno;}return ret;
}

strerror()函数可以将错误码转化成错误信息。
在这里插入图片描述
错误信息一输出用户就知道是哪出错了,echo $? 输出的退出码父进程bash也就知道了。

2.1.3 代码异常终止引入

代码异常终止,一般代码都没跑完,退出码也就没意义了。
我们举两个异常的例子:

#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{printf("before: %d\n", errno);FILE* fp = fopen("./log.txt", "r");if(NULL == fp){printf("after: %d, error string: %s\n", errno, strerror(errno));}int a = 10;a /= 0; // 除0错误return 0;
}

在这里插入图片描述

#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{printf("before: %d\n", errno);FILE* fp = fopen("./log.txt", "r");if(NULL == fp){printf("after: %d, error string: %s\n", errno, strerror(errno));}int* ptr = NULL;*ptr = 10; // 野指针return 0;
}

在这里插入图片描述

野指针一般是段错误。
代码跑起来之后就是进程,出问题是进程异常了,异常后它就不跑了,操作系统管理的进程,其实是操作系统把进程杀掉了(通过发送信号的方式杀掉的)。
我们查看一下信号:

在这里插入图片描述

可以看到SIG前缀是统一的,我们刚才的两个错误分别可以转换为8号与11号信号,FPE代表Floating point exception,SEGV代表Segmentation fault。
我们再来测试一下,看看其他的信号可不可以杀掉不是对应问题的进程:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{while(1){printf("I am a normal process: %d\n", getpid());}return 0;
}

在这里插入图片描述

我们发现,其实代码并没有错误,但是用户用的8号信号杀掉的进程,所以显示的就是8号所对应的异常信息。
结论进程出异常,异常信息会被操作系统检测出来,进而转换为信号然后杀掉进程。
最后,子进程把父进程交给的任务完成的怎么样,只要守好退出码信号编号为0就是正确,因为错误码从1开始的两个数字就可以很好的监督任务的完成程度。

2.2 进程常见退出方法

2.2.1 exit函数

我们先来查看一下exit怎么使用!
在这里插入图片描述

结论参数是进程的退出码,类似于main函数的return n。
了解了使用方法,我们来写一段代码试试:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{printf("I am a process, pid: %d, ppid:%d\n", getpid(), getppid());exit(12); // 参数是进程的退出码,类似于main函数的return n//return 0;
}

在这里插入图片描述
我们再来看一个场景:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int func()
{printf("call func function done!\n");// 任意地点调用exit,表示进程退出,不进行后续执行exit(21);
}int main()
{func();printf("I am a process, pid: %d, ppid:%d\n", getpid(), getppid());// 参数是进程的退出码,类似于main函数的return nexit(12);//return 0;
}

在这里插入图片描述

结论任意地点调用exit,表示进程退出,不进行后续执行。
我们可以在验证一下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int func()
{printf("call func function done!\n");// 任意地点调用exit,表示进程退出,不进行后续执行exit(21);
}int main()
{exit(31);func();printf("I am a process, pid: %d, ppid:%d\n", getpid(), getppid());// 参数是进程的退出码,类似于main函数的return nexit(12);//return 0;
}

在这里插入图片描述

经过这次的验证说明我们得出的结论是正确的。

2.2.2 _exit函数

依旧先查看怎么使用!
在这里插入图片描述

我们来使用一下试试:

#include <stdio.h>
#include <stdlib.h>int func()
{printf("call func function done!\n");return 11;
}int main()
{func();printf("I am a process, pid: %d, ppid:%d\n", getpid(), getppid());// 参数是进程的退出码,类似于main函数的return nexit(12);//return 0;
}

在这里插入图片描述

我们发现和exit的现象是一样的。
我们再来看看:

#include <stdio.h>
#include <unistd.h>int func()
{printf("call func function done!\n");//return 11;// 任意地点调用exit,表示进程退出,不进行后续执行_exit(21);
}int main()
{func();printf("I am a process, pid: %d, ppid:%d\n", getpid(), getppid());// 参数是进程的退出码,类似于main函数的return n//_exit(12);//return 0;
}

在这里插入图片描述

我们看到,_exit和exit 它两表现出的结果是一致的,但是这并不能说明它两没有区别!
为了让大家看到不一致性,我们继续写代码来观察:

#include <stdio.h>
#include <stdlib.h>int main()
{printf("you can see me!");sleep(3);exit(1);
}

在这里插入图片描述
在这里插入图片描述

我们打印的字符串没有\n,因为缓冲区的原因,字符串不会立即刷新出来,在进程退出后,exit对缓冲区强制刷新,才将字符串打印在屏幕上!
我们这次改为_exit来试试:

#include <stdio.h>
#include <unistd.h>int main()
{printf("you can see me!\n");sleep(3);_exit(1);
}

在这里插入图片描述
在这里插入图片描述
我们发现,_exit函数并不会在进程退出时对缓冲区做强制刷新!
结论:

  • exit是库函数(3号手册),_exit是系统调用(2号手册);
  • exit终止进程的时候,会自动刷新缓冲区。_exit终止进程的时候,不会自动刷新缓冲区(直接将数据扔掉了)。
  • 我们目前知道的缓冲区,绝对不在操作系统内部!(具体的后面再详谈)
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/453573.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端文艺复兴:Vue3真的需要Pinia吗?

前言 说起Pinia&#xff0c;熟悉 vue3 开发的程序员肯定不会陌生&#xff0c;甚至被vue官方推荐取代vuex&#xff0c;成为vue全家桶之一。 疑惑 还记得之前用 vuex 时&#xff0c;更改 state 还分同步和异步&#xff08;这里有尤雨溪的回答www.zhihu.com/question/48… &…

【TCP/IP】用户访问一个购物网站时TCP/IP五层参考模型中每一层的功能

当用户访问一个购物网站时&#xff0c;网络上的每一层都会涉及不同的协议&#xff0c;具体网络模型如下图所示。 以下是每个网络层及其相关的协议示例&#xff1a; 物理层&#xff1a;负责将比特流传输到物理媒介上&#xff0c;例如电缆或无线信号。所以在物理层&#xff0c;可…

创建TextMeshPro字体文件

相比于Unity的Text组件&#xff0c;TextMesh Pro提供了更强大的文本格式和布局控制&#xff0c;更高级的文本渲染技术&#xff0c;更灵活的文本样式和纹理支持&#xff0c;更好的性能以及更易于使用的优点。但unity自带TextMeshPro字体不支持中文。这里使用普通字体文件生成Tex…

docker更换镜像源

添加的镜像源 {"registry-mirrors": ["https://registry.cn-hangzhou.aliyuncs.com", "https://reg-mirror.qiniu.com/", "https://docker.mirrors.ustc.edu.cn"] }docker更换镜像源之后一定要重启守卫 systemctl daemon-reloaddock…

微服务的幂等性

微服务架构设计的中心思想是将服务进行拆分&#xff0c;但是在这个过程中&#xff0c;如果被依赖的服务发生奔溃&#xff0c;就会引起一系列问题。为了解决这个问题&#xff0c;就会引入重试的机制&#xff0c;重试又会引入幂等性的问题&#xff0c;下面我们就分析这个过程&…

【Linux】 信号的保存 | 捕捉

对于信号&#xff0c;主要涉及到信号的产生、保存和捕获&#xff0c;之前谈到了信号的产生&#xff0c;这里主要介绍信号产生后如何进行保存和捕捉处理的原理。 一、信号的保存 1.阻塞信号 相关概念 实际执行处理信号的动作称为信号递达Delivery信号从产生到递达的过程称为信号…

golang并发安全-sync.Once

什么是sync.Once sync.Once 是 Go 语言中的一种同步原语&#xff0c;用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法&#xff0c;即 Do&#xff0c;该方法接收一个函数参数。在 Do 方法被调用后&#xff0c;该函数将被执行&#xff0c;而且只会执行一…

计算机网络-差错控制(奇偶校验码 CRC循环冗余码)

文章目录 差错从何而来从传感器层面提高信道比来减少线路本身的随机噪声的一个例子热噪声和冲击噪声 数据链路层的差错控制检错编码-奇偶校验码检错编码-CRC循环冗余码例子注意 差错从何而来 噪声通常指的是任何未预期的、随机的信号干扰&#xff0c;这些干扰可能源自多种物理…

C++项目 -- 高并发内存池(二)Thread Cache

C项目 – 高并发内存池&#xff08;二&#xff09;Thread Cache 文章目录 C项目 -- 高并发内存池&#xff08;二&#xff09;Thread Cache一、高并发内存池整体框架设计二、thread cache设计1.整体设计2.thread cache哈希桶映射规则3.TLS无锁访问4.thread cache代码 一、高并发…

Log360,引入全新安全与风险管理功能,助力企业积极抵御网络威胁

ManageEngine在其SIEM解决方案中推出了安全与风险管理新功能&#xff0c;企业现在能够更主动地减轻内部攻击和防范入侵。 SIEM 这项新功能为Log360引入了安全与风险管理仪表板&#xff0c;Log360是ManageEngine的统一安全信息与事件管理&#xff08;SIEM&#xff09;解决方案…

51单片机之LED灯模块篇

御风以翔 破浪以飏 &#x1f3a5;个人主页 &#x1f525;个人专栏 目录 点亮一盏LED灯 LED的组成原理 LED的硬件模型 点亮一盏LED灯的程序设计 LED灯闪烁 LED流水灯 独立按键控制LED灯亮灭 独立按键的组成原理 独立按键的硬件模型 独立按键控制LED灯状态 按键的抖动 独立按键…

Unity中blendtree和state间的过渡

混合树状态之间的过渡 如果属于此过渡的当前状态或下一状态是混合树状态&#xff0c;则混合树参数将出现在 Inspector 中。通过调整这些值可预览在混合树值设置为不同配置时的过渡表现情况。 如果混合树包含不同长度的剪辑&#xff0c;您应该测试在显示短剪辑和长剪辑时的过渡表…