[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.rhkb.cn/news/252436.html

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

相关文章

JAVA面试汇总总结更新中ing

本人面试积累面试题 多线程微服务JVMKAFKAMYSQLRedisSpringBoot/Spring 1.面向对象的三个特征 封装&#xff0c;继承&#xff0c;多态&#xff0c;有时候也会加上抽象。 2.多态的好处 允许不同类对象对同一消息做出响应&#xff0c;即同一消息可以根据发送对象的不同而采用多种…

软考21-上午题-数组、矩阵

数组&#xff1a;一组地址连续的空间。 数组是定长线性表在维数上的扩展&#xff0c;即&#xff0c;线性表中的元素又是一个线性表。 一、数组 数组的特点&#xff1a; 数组数目固定&#xff0c;一旦定义了数组结构&#xff0c;不再有元素个数的增减变化。因此&#xff0c;数…

C# Onnx GroundingDINO 开放世界目标检测

目录 介绍 效果 模型信息 项目 代码 下载 介绍 地址&#xff1a;https://github.com/IDEA-Research/GroundingDINO Official implementation of the paper "Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection" 效果 …

MATLAB矩阵的操作(第二部分)

师从清风 矩阵的创建方法 在MATLAB中&#xff0c;矩阵的创建方法主要有三种&#xff0c;分别是&#xff1a;直接输入法、函数创建法和导入本地文件中的数据。 直接输入法 输入矩阵时要以中括号“[ ]”作为标识符号&#xff0c;矩阵的所有元素必须都在中括号内。 矩阵的同行元…

openssl3.2 - use openssl cmd create ca and p12

文章目录 openssl3.2 - use openssl cmd create ca and p12概述笔记实验的openssl环境建立CA生成私钥和证书请求生成CA证书用CA签发应用证书用CA对应用证书进行签名将已经签名好的PEM证书封装为P12证书验证P12证书是否可用END openssl3.2 - use openssl cmd create ca and p12 …

Redis(三)(实战篇)

查漏补缺 1.spring 事务失效 有时候我们需要在某个 Service 类的某个方法中&#xff0c;调用另外一个事务方法&#xff0c;比如&#xff1a; Service public class UserService {Autowiredprivate UserMapper userMapper;public void add(UserModel userModel) {userMapper.…

睿尔曼超轻量仿人机械臂—外置按钮盒使用说明

睿尔曼RM系列机械臂的控制方式有很多种&#xff0c;包括&#xff1a;示教器、JSON、API等。在此为大家介绍外置按钮盒的使用方法。 按钮盒接线安装 按钮盒外观如下图所示&#xff0c;有&#xff1a;急停、暂停、开始、继续。四个功能按钮。用户可通过这四个按钮来实现对机械臂运…

[嵌入式AI从0开始到入土]13_orangepi aipro开箱测评

[嵌入式AI从0开始到入土]嵌入式AI系列教程 注&#xff1a;等我摸完鱼再把链接补上 可以关注我的B站号工具人呵呵的个人空间&#xff0c;后期会考虑出视频教程&#xff0c;务必催更&#xff0c;以防我变身鸽王。 第1期 昇腾Altas 200 DK上手 第2期 下载昇腾案例并运行 第3期 官…

如何在电脑上恢复查看iPhone短信?4个有效方法给你!

在当今科技发达的世界&#xff0c;能够在计算机上查看 iPhone 短信将彻底改变游戏规则。无论是存档珍贵的对话还是管理与工作相关的聊天&#xff0c;这都是一项至关重要的技能。在本指南中&#xff0c;我们将引导您了解如何在计算机上查看 iPhone 短信的四种高效方法。通过执行…

1.0 Zookeeper 分布式配置服务教程

ZooKeeper 是 Apache 软件基金会的一个软件项目&#xff0c;它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。 ZooKeeper 的架构通过冗余服务实现高可用性。 Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来&#xff0c;构成一个高…

【AI数字人-论文】Geneface论文

文章目录 前言pipelineaudio-to-motionMotion domain adaptation可视化 Motion-to-imageHead-NeRFTorso-NeRF 结果对比 前言 语音驱动的说话人视频合成旨在根据一段输入的语音&#xff0c;合成对应的目标人脸说话视频。高质量的说话人视频需要满足两个目标&#xff1a; &#…

【无刷电机学习】电流采样电路硬件方案

【仅作自学记录&#xff0c;不出于任何商业目的】 目录 AD8210 INA282 INA240 INA199 AD8210 【AD8210数据手册】 在典型应用中&#xff0c;AD8210放大由负载电流通过分流电阻产生的小差分输入电压。AD8210抑制高共模电压(高达65V)&#xff0c;并提供接地参考缓冲输出&…

Spring Data Envers 数据审计实战2 - 自定义监听程序扩展审计字段及字段值

上篇讲述了如何在Spring项目中集成Spring Data Envers做数据审计和历史版本查看功能。 之前演示的是业务表中已有的字段进行审计&#xff0c;那么如果我们想扩展审计字段呢&#xff1f; 比如目前对员工表加入了Audited审计&#xff0c;员工表有个字段为dept_id&#xff0c;为…

在线JSON转SQL工具

在线JSON转SQL - BTool在线工具软件&#xff0c;为开发者提供方便。在线JSON转SQL工具可以将JSON文件中的数据或者JSON对象转换为SQL插入语句&#xff0c;方便用户将数据导入到数据库中。用户可以通过简单的界面上传JSON文件&#xff0c;或者文本框输入&#xff0c;点击JSON转S…

数据结构高级算法

目录 最小生成树 Kruskal(克鲁斯卡尔)(以边为核心) 9) 不相交集合(并查集合) 基础 Union By Size 图-相关题目 4.2 Greedy Algorithm 1) 贪心例子 Dijkstra Prim Kruskal 最优解(零钱兑换)- 穷举法 Leetcode 322 最优解(零钱兑换)- 贪心法 Leetcode 322 3)…

数字孪生网络攻防模拟与城市安全演练

在数字化浪潮的推动下&#xff0c;网络攻防模拟和城市安全演练成为维护社会稳定的不可或缺的环节。基于数字孪生技术我们能够在虚拟环境中进行高度真实的网络攻防模拟&#xff0c;为安全专业人员提供实战经验&#xff0c;从而提升应对网络威胁的能力。同时&#xff0c;在城市安…

day02-大盘板块功能实现

day02-大盘板块功能实现 今日目标 完善基于前后端分离用户验证码登录功能;理解验证码生成流程,并使用postman测试;掌握SwaggerYapi使用理解并实现国内大盘数据展示功能;理解并实现国内板块数据展示功能;理解后端接口调试和前后端联调的概念; 第一章 验证码登录功能 1、前后…

leetcode1079:游戏玩法分析——求留存率

求留存率 题目描述题解 题目描述 表&#xff1a;Activity --------------------- | Column Name | Type | --------------------- | player_id | int | | device_id | int | | event_date | date | | games_played | int | --------------------- &#xff08;player_id&…

第5课 使用FFmpeg将rtmp流再转推到rtmp服务器

本课对应源文件下载链接&#xff1a; https://download.csdn.net/download/XiBuQiuChong/88801992 通过前面的学习&#xff0c;我们已经可以正常播放网络rtmp流及本地mp4文件。这节课&#xff0c;我们将在前面的基础上实现一个常用的转推功能&#xff1a;读取rtmp流或mp4文件并…

嵌入式软件的设计模式与方法

思想有多远&#xff0c;我们就能走多远 4、状态与工作流类设计模式 4.1 状态与事件 行为随条件变化而改变&#xff0c;这里状态切换的模式也称为状态机。有限状态机 (Finite State Machine&#xff0c;FSM) 是由3 个主要元素组成的有向图: 状态、转换和动作。 状态是系统或者…