进程的创建、终止

目录

  • 前言
  • 1. 进程创建
  • 2. 进程终止
  • 3. exit && _exit 的异同
    • 3.1 相同点
    • 3.2 不同点

前言

紧接着进程地址空间之后,我们这篇文章开始谈论进程控制相关的内容,其中包括进程是如何创建的,进程终止的几种情况,以及进程异常终止的本质,还有 C 语言库中的 strerror 以及 errno 全局变量的相关内容,最后对比系统调用 _exit 与 C 库的 exit 的异同点。


1. 进程创建

在之前文章 进程概念(三)----- fork 初识,我们初始了 fork(),大致了解了一个进程是怎么被创建出来的,不管是我们的程序运行起来后,操作系统为我们自动创建的,还是代码层面我们手动创建的进程,都是通过 fork() 实现的,也回答了与 fork 相关的几个内容,为什么要有两个返回值(为了让父子进程分流工作);如何做到返回两次的(在return之前,子进程就已经被创建完成,因此父子进程都执行了一遍return语句)以及 一个变量如何做到两个不同的内容的(写诗拷贝实现的)。

有了之前的 fork 初始,以及进程地址空间,页表等铺垫,我们现在就足以理解一个进程是如何被创建的了。

创建进程,最终都离不开 fork() 函数,当一个进程调用 fork 之后,操作系统就会做以下几件事:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程(包括进程地址空间,页表都会拷贝一份给子进程)
  • 添加子进程到系统进程列表当中(本质就是讲子进程的 PCB 链入到 cpu 的运行队列中)
  • fork返回,开始调度器调度

所以 fork 之后,父子进程就开始共享代码,但数据却不一定是共享的,也就是可能发生写诗拷贝。
在这里插入图片描述
关于写诗拷贝,再谈一个细节。在创建子进程时,当子进程开始要拷贝父进程的进程地址空间,页表的时候,父进程会把页表中的所有读写权限的字段暂时设置为只读(原本只读的字段保持不变),然后再让子进程开始继承其进程地址空间和页表。而后续当其中一个进程开始对这些只读字段的地址进行写入时(操作系统能够识别到原本属于读写权限的),操作系统不做异常处理,它会重新开辟一块内存空间,将要写入的数据拷贝一份到新地址中,然后修改这个进程的页表中相应字段的物理地址,再把这个字段恢复为读写权限,这就完成了所谓的 写时拷贝

而既然有写时拷贝,那就注定在技术层面上可以不要写时拷贝,在创建子进程的时候,直接一次性的将父进程的全部数据拷贝一份给子进程不就完了吗,还省事!

其实这样做反而 “不省事” ! ------>

  • 可能导致大量重复的数据在内存中,造成资源浪费
  • 拷贝量可能很大,导致整机性能下降(虽然之后写时拷贝的时候也要拷贝,但拷贝量注定不会很大,效率是不会降低的)

而在而来 fork 可能会出现调用失败的原因:1. 系统中有太多的进程; 2. 实际用户的进程数超过了限制

代码层面上想要创建多个子进程,可以借助循环创建。demo用例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>#define N 5void runChild()
{int cnt = 10;while(cnt){printf("I am child: %d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{int i = 0;for(; i < N; i++){pid_t id = fork();if(id == 0){runChild();exit(0);} }sleep(100);return 0;
}

关于父子进程到底谁先运行这件事,是具体某一款操作系统的调度器怎么设计决定的,并没有唯一的标准,而一定要说哪个进程先运行,因为创建出来的子进程的优先级默认都是一样的,只能是谁先被调度器调度,放在 cpu 的运行队列中,谁就先被运行。


2. 进程终止

一个进程的终止无非就是以下这三种情况:

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

而我们应该关注的是后面两种情况,代码运行完毕且结果正确我们一般不关心(就好比考试完毕且成绩达到预期,我们也不会去关心),但是第二种情况,代码运行完毕且结果不正确,我们肯定要关心吧,总得知道为什么不正确吧,包括代码异常终止。

int main()
{printf("this is a test!\n");return 0; 	// return 0 的 0 就是进程的退出码,表征进程的运行结果是否正确. 0->success
}

在这里插入图片描述

echo $? 其中的 $? 代表保存最近一次进程退出时的退出码,而上面我们执行了 test 这个可执行程序,该进程执行完毕后返回了一个 0,代表这这个进程执行完毕,并且结果正确。而如果我们代码中不是 retrun 0,而是 return 1/2/3,那么echo $? 查出来的结果也不一样的。换言之,可以用 return 不同的返回值数字,表征不同的出错原因,这些数字就称为退出码。 而 main 函数返回值的本质就是:进程运行完成时是否是正确的结果,如果不是,可以用不同的退出码表示的出错原因。

那进程中,谁会关心我这个进程的运行结果呢?? ----- 该进程的父进程

在这里插入图片描述

当我们 main 函数中 return 2 ,那我们 echo $? 查出来的退出码就是 2,但是之后我继续执行 echo $? 就不再是 2 了,而是 0,这是因为第一条 echo 命令执行完是正确的,没有发生错误,所以接下来的 echo $? 打印的就是最近一次进程退出时的退出码(echo执行起来时,本身就是一个进程)。

但是,退出码都是些 0 1 2 3 4 的纯数字,理解起来也太抽象了吧,所以退出码只是给计算机自己看的,在用户层,就需要将这些特定含义的数字转换成对应错误原因描述的字符串信息,方便我们观看使用。

而在 C 预言中,就有一个查看错误码的库函数 strerror,并且我们可以把这个函数的信息打印出来看一下。

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

当我们 ls 查看目录中不存在的文件名,它的错误信息 与 上面我们打印出来的 strerror 函数中的内容是匹配的上的!No such file or directory 这条错误信息的退出码正好是 2 。换言之,系统提供的错误码和错误码描述是有对应关系的。

但为什么父进程会关心子进程的执行结果呢??其实父进程它只是一个跑腿的,可以理解为是因为它有义务要收集子进程的执行结果,并且向上反馈,而不是它真的想关心子进程的执行结果。换言之,真正关心一个进程的执行结果的人一定是用户。就好比我们上面 ls 查看当前目录不存在的文件,执行失败了,用户才会根据执行失败的错误信息,调整自己执行程序的方式!

那为什么要创建子进程?因为有需求,不就是因为用户要创建子进程出来干活吗。所以将来这个子进程执行某个任务的结果,我作为用户,我如何得知!? ------ 通过父进程将进程的退出信息转交给用户,让用户根据错误信息做下一阶段的执行决策

接下来,我们可以来看看退出码的应用场景。在 C 库中提供了 errno 全局变量,在 C 语言中,我们可能会调用一些库函数,比如 malloc,fopen,这些库函数在调用时,内部都是有可能会执行错误的,而这个 errno 就是记录最后一次执行的错误码。要记录错误码的就是因为只要执行错误,那用户就有需求要了解错误信息是什么。

int main()
{int ret = 0;char* p = (char*)malloc(1000*1000*1000*4);if(p == NULL){printf("malloc error, %d: %s\n", errno, strerror(errno));ret = errno;}else{printf("malloc success, %d: %s\n", errno, strerror(errno));}return ret;
}

在这里插入图片描述

当我们 malloc 申请很大的内存时,极大困难就是 12 的错误码:Cannot allocate memory。而这个错误码是 C 语言提供的 errno 这样的全局变量记录下来的,结合 strerror 根据错误码查询具体错误信息得来的!

截止现在,我们还没有讲进程终止的第三种情况:代码异常中止, 现在的问题是,代码异常中止了,main 函数的退出码还有意义吗??

前面两种进程终止的情况都好说,因为起码代码执行完了,我们可以通过退出码来判定是否执行正确。

  • 而代码异常,是不是可以理解为代码没跑完,进程就已经退出了,根本就没有执行 main 函数的 return 语句。
  • 但是会不会也有可能,执行完 main 函数的 return 语句之后,才发生的异常中止的呢?好像也有可能?毕竟 main 函数也是函数,它也会被调用。return 之后,在语言层面上认为这个程序结束了,但在系统层面上,这个进程不一定退出了!

所以现在的关键是,作为用户的我,我该怎么知道,这个程序到底有没有执行最后的 return 语句呢!?就是因为我们对代码异常在哪个位置的不确定性,假设今天真的是 return 之后才异常的,退出码给返回到了父进程,进而转交给用户,作为用户的你,你真的敢用吗?换言之,你敢信吗??凭什么你就敢确定这个程序真的执行了 return 语句呢?

作为用户的我们,不能百分百确定确定在哪一行代码发生的异常,所以即便真的有退出码,这个退出码照样没有意义!(因为我们不敢相信。就好比考试的时候,你作弊了,但是你跟老师解释说是考试即将结束作弊的,你只抄了最后一道题,老师会信吗??换言之,只要你作弊了,你何时作弊,对于老师来说已经不重要了,没有意义了!你的话也不再可信,他只知道你作弊了!再者,难道你只抄了最后一题,最后100分的试卷考了95分,去掉最后的5分,你也还有90分,是年级百强选手,难道学校管理层还要把你加入百优表彰宣传单里面吗?然后同时再贴出一张违纪通知书:上面写着你的名字??你觉得这件事合理吗??换言之,成绩 与 作弊 这两件事,只能有一件事存在!同时存在就毫无意义!!同理,代码异常中止了也如此,只要中止了,那么退出码将毫无意义!!)


所以当进程异常终止退出时,我们也就不关心退出码了,但是我们应该要关心进程为什么异常了,以及进程发生了什么异常。

当我们对空指针进行解引用访问时,会出现 Segmentation fault (core dumped) 这样的错误信息;当我们进行除 0 运算时,会有 Floating point exception (core dumped) 。而类似这样的代码异常中止的情况,本质就是进程收到了对应的信号!

[outlier@localhost process3]$ kill -l1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	kill -l 可以查看给进程发送的信号,其中的 8 号信号,SIGFPE == Floating point exception
11 号信号就是我们常见的段错误 SIGSEGV == Segmentation fault

那么如何验证所谓的进程异常中止就是收到了某种信号呢??

当进程跑起来之后,我们尝试对进程发送 8 号信号 和 11 号信息,并观察现象

在这里插入图片描述


3. exit && _exit 的异同

  • exit 是 C 语言的库函数
  • _exit 是 linux 系统的系统调用

3.1 相同点

在不涉及缓冲区刷新的问题,exit 和 _exit 的作用都是一样的,都是用于进程退出的函数,与 return 不同的是,return 更多的代表函数的返回,当 main 函数调用一个函数,使用 return 之后,程序会回到调用处继续执行后续代码,而 exit / _eixt 则是直接进程退出了,不会再执行 exit / _eixt 之后的代码。

3.2 不同点

int main()
{printf("hello, linux!");sleep(1);exit(12);
}

在这里插入图片描述

int main()
{printf("hello, linux!");sleep(1);exit(12);
}

在这里插入图片描述

当调用的是 C 库的 exit,那么在退出进程时,会刷新缓冲区,关闭文件流等各种流,而在掉系统调用 _exit 则不会刷新缓冲区,这也是为什么在 _exit 之后,本该打印输出的信息并没有打印在屏幕上,因为 printf 一定是先把数据写入缓冲区中,达到某种界限时,在进行刷新!

在这里插入图片描述

程序中调用 _exit,直接在进程层面上终止进程; C 库当中的 exit 会先把应用层中打开的各种流关闭 以及 刷新缓冲区等操作,再调 _exit 终止进程。 可以理解为 exit 就是对 _exit 作了一定的封装!

那么我们需要知道,这个缓冲区一定不在哪里?? ---- 一定不在内核中!

为什么这么说呢,因为如果这个缓冲区在内核中,那么内核就一定需要维护这个缓冲区,只要是维护,就会有刷新,就不存在上面的现象了。

缓冲区在哪的问题,在后续关于 IO 方面的文章会介绍。


关于进程创建、进程终止等话题暂且谈论至此,后续还会讲进程控制中的进程等待。

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

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

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

相关文章

Android低内存设备系统优化

切记,所有的优化都遵循一条准则: 空间换时间,时间换空间。 一、前言 我们为什么会觉得卡顿、不流畅? 卡顿等性能问题的最主要根源都是因为渲染性能,Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出信号,触发对UI进行渲染,如果每次渲染…

Thinkphp6 反序列化漏洞分析

本文来自无问社区&#xff0c;更多实战内容可前往查看http://wwlib.cn/index.php/artread/artid/10431.html 版本&#xff1a;Thinkphp6&PHP7.3.4 TP 环境搭建利用 composer 命令进行&#xff0c;同时本次分析在 windows 环境下进行 composer create-project topthink/t…

Android 架构模式之 MVP

目录 架构设计的目的对 MVP 的理解代码ModelViewPresenter Android 中 MVP 的问题试吃个小李子ModelViewPresenter效果展示 大家好&#xff01; 作为 Android 程序猿&#xff0c;你有研究过 MVP 架构吗&#xff1f;在开始接触 Android 那一刻起&#xff0c;我们就开始接触 MVC…

高频变压器无功补偿怎么做

高频变压器的无功补偿主要是为了提高功率因数、减小无功损耗、提高电源利用率。在高频电路中&#xff0c;由于频率较高&#xff0c;传统的无功补偿方法需要进行一定的调整和优化。以下是高频变压器无功补偿的一些方法和建议&#xff1a; 1、无功补偿电容器 高频电容器选择&…

具有手势识别的动捕设备——mHand Pro VR数据手套

数据手套是指通过手套内置的传感器&#xff0c;实时采集手部运动数据的动捕设备&#xff0c;通常被应用于虚拟仿真、虚拟现实vr交互、动画制作等领域。其中&#xff0c;基于惯性动作捕捉技术研发的数据手套&#xff0c;凭借其高性价比的优势&#xff0c;在市面上的应用更为广泛…

STM32G474按钮输入和点灯

在获取到工程模板后&#xff0c;学习某个CPU的第一步通常都是IO口操作。因此按钮输入和点灯&#xff0c;就是本次学习的第一个程序。先从简单入手。 和GPIO操作有关的函数如下: __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟 __HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIOB时钟 _…

深度理解指针(2)

hello各位小伙伴们&#xff0c;关于指针的了解我们断更了好久了&#xff0c;接下来这几天我会带领大家继续我们指针的学习。 目录 数组名的理解 使用指针访问一维数组 一维数组传参的本质 二级指针 指针数组 使用指针数组来模仿二维数组 数组名的理解 我们首先来看一段…

【开源社区】Elasticsearch(ES)中 exists 查询空值字段的坑

文章目录 1、概述2、使用 null_value 处理空值3、使用 exists 函数查询值为空的文档3.1 使用场景3.2 ES 中常见的空值查询方式3.3 常见误区3.4 使用 bool 查询函数查询空值字段3.5 exists 函数详解3.5.1 bool 查询的不足3.5.3 exists 的基本使用 3.6 完美方案 1、概述 本文主要…

单例模式 详解

单例模式 简介: 让类只初始化一次, 然后不同的地方都能获取到同一个实例 这是非常常用的一种模式, 系统稍微大一点基本上都会用到. 在系统中, 不同模块的总管理类都已单例模式居多 这里我们不仅使用c实现单例模式, 也会用python2实现一遍 python代码 想要看更详细的python单…

手动下载Sentinel-1卫星精密轨道数据

轨道信息对于InSAR&#xff08;干涉合成孔径雷达&#xff09;数据处理至关重要&#xff0c;因为它影响从初始图像配准到最终形变图像生成的整个过程。不准确的轨道信息会导致基线误差&#xff0c;这些误差会以残差条纹的形式出现在干涉图中。为了消除由轨道误差引起的系统性误差…

Swift 6.0 如何更优雅的抛出和处理特定类型的错误

概述 从 Swift 语言诞生那天儿起&#xff0c;它就不厌其烦一遍又一遍地向秃头码农们诉说着自己的类型安全和高雅品味。 不过遗憾的是&#xff0c;作为 Swift 语言中错误处理这最为重要的一环却时常让小伙伴们不得要领、满腹狐疑。 在本篇博文中&#xff0c;您将学到如下内容&…

基于网格尺度的上海市人口分布空间聚集特征分析与冷热点识别

在上篇文章提到了同一研究空间在不同尺度下的观察可能会带来不同的见解和发现&#xff0c;这次我们把尺度缩放到网格&#xff0c;来看网格尺度下的空间自相关性、高/低聚类&#xff0c;这些&#xff0c;因为尺度缩放到网格尺度了&#xff0c;全国这个行政区范围就显的太大了&am…

基于Shader实现的UGUI描边解决方案遇到的bug

原文链接&#xff1a;https://www.cnblogs.com/GuyaWeiren/p/9665106.html 使用这边文章介绍的描边解决方案时遇到了一些问题&#xff0c;就是文字的描边经常会变粗&#xff0c;虽然有的时候也可以正常显示描边&#xff0c;但是运行一会儿描边就不正常了&#xff0c;而且不正常…

UDP+TCP

一、UDP协议 1.recvfrom:recvform(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen); 参数&#xff1a;socket的fd; 保存数据的空间地址 &#xff1b; 空间大小&#xff1b; 默认接收方式&#xff08;默认阻塞&#xf…

【案例56】安全设备导致请求被拦截

问题现象 访问相关报表 第二次访问发现有相关的连接问题 问题分析 服务器访问相关节点&#xff0c;发现相关节点无此问题。从客户的客户端访问缺有问题。在nclog中发现如下日志&#xff0c;链接被重置。 直接访问服务器无丢包现象。客户端未开防火墙。装了杀毒软件已经卸载。…

简单记录:两台服务器如何超快速互传文件/文件夹

在服务器间传输文件和文件夹是一个常见的任务&#xff0c;尤其是在需要同步数据或进行备份时。以下是使用 scp 命令在两台服务器之间进行文件传输的基本步骤。 服务器A 至 服务器B&#xff1a;文件传输指南 前提条件 确保服务器A和服务器B之间网络互通。确认您有权限访问目标…

C语言 之 整数在内存中的存储、大小端字节序和字节序的判断

文章目录 整数在内存中的存储大小端字节序和字节序判断大小端有大小端的原因高位和地位怎么区分&#xff1f;图例判断机器大端还是小端的例题 整数在内存中的存储 整数的2进制表示方法有三种&#xff0c;即 原码、反码和补码 三种表示方法均有符号位和数值位两部分&#xff0c…

微信小程序获取当前位置并自定义浮窗

1、在腾讯地图api申请key&#xff08;添加微信小程序的appid&#xff09;。 每个Key每日可以免费使用100次&#xff0c;超过次数后会导致地图不显示。可以多申请几个Key解决。WebService API | 腾讯位置服务腾讯地图开放平台为各类应用厂商和开发者提供基于腾讯地图的地理位置…

当AI成为你的私人医生,与AI“医”路同行的奇妙体验

“ 从挂号到诊疗&#xff0c;再到后续的健康管理&#xff0c;人工智能&#xff08;AI&#xff09;正以一种全新的方式融入我们的生活。上海市第一人民医院的创新实践&#xff0c;便是这一变革的生动注脚。 ” AI就医助理&#xff1a;从“助手”到“伙伴” 当你踏入医院大门…

猜数3次-python

题目要求&#xff1a; 定一个数字&#xff08;1-10&#xff0c;随机产生&#xff0c;通过3次判断来猜出数字&#xff09; 数字随机产生&#xff0c;范围1-10有三次机会猜测数字&#xff0c;通过3层嵌套判断实现每次猜不中会提示大了或者小了 ps&#xff1a;补充随机函数 imp…