Linux:进程创建 进程终止

news2025/1/12 11:56:54

Linux:进程创建 & 进程终止

    • 进程创建
      • fork
      • 写时拷贝
    • 进程终止
      • 退出码
        • strerror
        • errno
      • 异常信号
      • exit


进程创建

fork

fork函数可以用于在程序内部创建子进程,其包含在头文件<unistd.h>中,直接调用fork()就可以创建子进程了。

示例代码:

#include <stdio.h>      
#include <unistd.h>      
      
int main()      
{      
    printf("before: ppid = %d,pid = %d\n", getppid(), getpid());    
    
    fork();    
    
    printf("after:  ppid = %d,pid = %d\n", getppid(), getpid());    
        
    return 0;    
}          

以上代码中,我们在fork前输出了一个before以及进程的PIDPPID。在fork后,又输出了after以及进程的PIDPPID

运行结果:

在这里插入图片描述

可以看到,我们的before输出了一次,也就是我们调用的进程./test.exe输出的,而after输出了两次,但是我们只有一个after语句,说明有两个不同的进程执行了这个语句,也就是fork成功创建了一个进程

对于第一条语句before,毫无疑问这是进程./test.exe输出的,PID22840

第二条语句after,其PIDPPID都和./test.exe一致,说明这个语句也是原先的./test.exe输出的。

第三条语句after,其PID22842,之前没有出现过,说明这个是通过fork创建出来的进程,其PPID22840,也就是./test.exe说明fork创建出来的进程,是原先进程的子进程

以上示例可以总结为:

  1. fork之后,会出现两个进程
  • 一个是原先的进程
  • 另外一个是通过fork创建的进程
  1. 新创建的进程,是原先进程的子进程

fork函数也是有返回值的,其返回规则如下:

  1. 对于父进程,返回值为新的进程的PID
  2. 对于子进程,返回值为0

此时我们就可以根据fork的返回值,来判断父子进程了:

代码示例:

#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
    pid_t id = fork();    
    
    if(id == 0)    
    {    
        printf("child:  ppid = %d,pid = %d\n", getppid(), getpid());    
    }    
    else    
    {    
        printf("father: ppid = %d,pid = %d\n", getppid(), getpid());    
    }    
                                                                                                            
    return 0;                                                           
}                

输出结果:

在这里插入图片描述

子进程输出了child:开头的语句,父进程输出了father:开头的语句。

我们确实通过这样的分支语句,利用父子进程的fork返回值不同的特性,完成了父子进程输出不同的代码。

其实fork创建子进程的时候,是以父进程为模板的,子进程会继承父进程的PCB,然后把PCB内部需要修改的地方改为自己的,比如PIDPPID是不同的。

子进程还和父进程共用代码段,因为两者的代码逻辑是一样的。比如说刚才的示例中,父子进程都要执行if-else的判断,两者都共用这一段代码。

但是两者的数据不一定相同,一开始父子进程共用一段数据,一旦父子进程有一方要对数据进行修改,那么就发生写时拷贝,此时数据就互不影响了。如果某个数据从头到尾都没有被修改,那么这个数据从头到尾都被父子进程共享,不会额外开辟内存。

现在就有一个问题了:为什么一个id既可以得到父进程的返回值,又可以得到子进程的返回值,难道fork函数可以一次返回两个值吗?

PCB内部有一个叫做内存指针的成员:

  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

其可以标识当前代码执行到了哪里,那么在父进程执行到fork的时候,此时就要开始创建子进程了,子进程会继承父进程的PCB

由于父进程此时执行到fork,那么内存指针就指向fork这个语句。由于子进程继承父进程PCB,因此刚创建的子进程继承的内存指针也指向fork,所以子进程是从fork开始往后执行的

那么下一个问题就是:fork函数是如何做到,一个函数返回两个值的呢?

回答这个问题之前,我先反问你一个问题,创建子进程是在什么时候创建的?

你也许会回答,就是fork的时候创建的,但是深究一下,你就会发现,一定是在fork函数内部创建的子进程。这个内部很关键,也就是说fork函数还没有return返回的时候,就已经是两个进程了

于是两个进程共用这个fork的代码,但是两个进程的数据不一样,所以其实pid_t id = fork();这个过程中,父子进程分别return了一次。所以本质上不是fork函数返回了两个值,而是同一段return代码,被父子进程分别调用了


写时拷贝

通过fork创建子进程的时候,会继承父进程的代码数据页表PCB等等,因此我们会发现fork之后,父子进程指向的代码是一样的。

比如以下代码:

#include <stdio.h>      
#include <unistd.h>       
      
int main()      
{    
    printf("process start!\n");    
    fork();    
    
    printf("hello world!\n");    
    printf("hello Linux!\n");    
    printf("hello process!\n");    
    
    return 0;    
}    

以上代码中,进程先输出了一句"process start!\n",随后在fork之后又输出了三条语句。

输出结果:

在这里插入图片描述

可以看到的是,fork前的语句只输出了一次,而fork后的语句都输出了两次,也就是被父子进程分别输出了一次,由此可证子进程会继承父进程的代码。

在这里插入图片描述

上图中,左侧是刚fork后的父子进程,子进程创建时页表几乎完全一致,最后导致不论是数据还是代码都指向了内存中的同一块区域。

但是页表中大部分的项权限都被改为了只读,当子进程想要修改数据,就会在页表中访问该虚拟地址,但是页表发现该地址是只读的,于是页表暂时终止进程访问内存的请求,这个过程叫做缺页中断

接着向上层汇报,操作系统介入。操作系统再进而分析得知,子进程想要对这块内存写入,于是发生写时拷贝。操作系统重新开一块内存给子进程,然后修改页表中的数据,一是修改虚拟地址与物理地址的映射关系,二是修改该内存的权限,从只读变为读写

右图中,蓝色区域被拷贝了一份到内存的其他区域,然后子进程的页表不再指向原先的区域,而是指向新开辟的区域。

那么借此机会,我再深入讲解一下C/C++中的const语法。

比如以下代码:

const char* str = "Hello World!";
*str = "Hello Linux";

毫无疑问这是一段错误的代码,因为strconst修饰的指针,不能通过该指针修改指向的值。但是从底层来说,其实本质上是在页表中,进程没有该地址的读写权限,只有只读权限。如果我们没有const这个语法,那么进程就有可能会向页表发出写入的申请,可是页表发现进程没有该地址的写入权限。因此页表会缺页中断,操作系统分析后发现这是非法访问,直接终止进程。

可见这是一个比较危险的过程,一旦访问没有权限的内存,很有可能就会直接终止程序。最典型的就是指针越界,访问野指针等等。而C/C++使用一个const修饰的语法,将问题提前暴露出来,对于一些已经确定不能写入的常量区地址,要求用const修饰,只要不用const修饰,在编译时就不允许通过,将问题提前暴露出来,从而提高程序的健壮性。


进程终止

讲解完进程的创建后,我们来看看进程是如何退出的,以及退出后会留下些什么。

退出码

在平常的C语言代码中,main函数最后的return其实返回的是退出码

  • 如果错误码为0,表示程序正常退出
  • 如果错误码非0,表示程序异常退出

因此大部分时候我们会return 0,表示程序正常返回。

环境变量?内部存储了上一个进程的退出码,我们可以在该进程执行完后,使用echo $?来输出上一个进程的退出码。

假设在test.exe中执行如下代码:

int main()
{

	return 123;
}

那么该进程的退出码就是123,我们输出试试看:

在这里插入图片描述

通过main函数的return,我们确实可以控制进程的退出码,那么这个退出码有什么用呢?

其实每一个错误码都对应了一个进程的错误信息,可以通过strerror函数来输出错误码对应的错误信息

strerror

strerror函数包含在头文件<string.h>中,传入一个错误码作为参数,返回指向错误信息的字符串。

我们用以下代码来输出前十条错误信息:

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

输出结果如下:

在这里插入图片描述

比如0表示Success即执行成功,1表示Operation not permitted即操作不被允许。

比如现在我们ls一个不存在的文件夹:

在这里插入图片描述

此时ls报错No such file or directory,即该文件或目录不存在,这条语句和错误码2完全一致,而我们再echo $?发现,上一个进程的退出码就是2

那么现在又有一个问题:进程通过main函数的返回值来判断错误,那么对一般的函数,我们要如何得知函数的执行情况?

errno

函数的返回值是用于返回调用结果的,不适合用作返回错误码,全局变量errno包含在头文件<errno.h>中,其存储了上一次函数调用的错误码

比如以下代码:

#include <stdio.h>    
#include <string.h>    
#include <errno.h>    
    
void func()    
{    
    errno = 10;    
}    
    
int main()    
{    
    func();    
    
    printf("%d:%s\n", errno, strerror(errno));    
                                                                                                            
    return 0;    
}    

以上代码中func函数修改了全局变量errno,此时就可以在函数外部检测errno来判断函数的调用情况了,此处在func中令errno = 10,然后在函数调用结束后输出errno以及其对应的错误信息:

在这里插入图片描述

而在库函数中,也会使用errno的,比如fopen函数,当fopen发生错误时,就会修改errno来返回错误信息。

示例:

#include <stdio.h>    
#include <string.h>    
#include <errno.h>    
    
void func()    
{    
    errno = 10;    
}    
    
int main()    
{    
	fopen("123.c", "r");
    printf("%d:%s\n", errno, strerror(errno));    
                                                                                                            
    return 0;    
}    

在当前目录下,不存在一个叫做123.c的文件,我们用fopenr形式打开,那么fopen是打不开文件的,于是就会向errno进行写入。

执行结果:

在这里插入图片描述

输出了错误码2,即没有对应的文件或目录。


异常信号

对于任意一个进程,都只有两种退出情况:

  1. 代码执行完毕,正常退出
  2. 进程发生了异常,被迫退出

比如说访问空指针,野指针,除零错误等等,这些都会导致进程直接终止。

除零错误:即把0当作除数,这是不允许的

比如在test.exe中执行:

double a = 5 / 0;

输出结果:

在这里插入图片描述

其报出一个错误Floating point exception说明可能存在 / 或者 % 的除数为 0 的情况,这就说明进程发生了异常。

进程发生异常的本质,是收到了异常信号

我们可以通过kill -l来查看异常信号:

在这里插入图片描述

如果想要对一个进程发送异常信号,只需要kill -xxx ###即可向pid###的进程发送xxx号信号。

比如8号信号SIGFPE,其中SIG表示signal信号,而FPEFloating point exception的缩写,也就是刚刚的除零错误。刚刚除零错误异常退出,就是接收到了8号信号。

现在我们在test.exe中执行以下代码,来验证异常信号:

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h>    
#include <errno.h>    
    
int main()    
{    
    while(1)    
    {    
        printf("pid = %d\n", getpid());    
        sleep(1);    
    }    
    return 0;    
}              

该程序会陷入死循环,然后每隔一秒输出自己的pid,得到pid是为了可以发送信号。当进程执行起来后我们在另外一个对其发送8信号:

在这里插入图片描述

左侧窗口执行进程后,pid=8495,右侧窗口执行指令kill -8 8495,就向8495发送了8号信号,此时进程直接退出,并输出Floating point exception。可以看到,明明是一个死循环,我们可以通过发送异常信号直接终止,而代码中明明没有除零错误,我们也可以通过信号对其强行加上异常。


exit

exit函数可以用于在进程中直接退出,其包含在头文件<stdlib.h>中。

示例:

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
                                                                                                      
void func()    
{    
    exit(5);    
}    
    
int main()    
{    
    func();    
    
    return 0;    
}    

在函数func中,调用了exit(5),此时整个进程都会直接退出,并且返回错误码5

输出结果:

在这里插入图片描述

可以看到,进程的退出码是5,说明通过exit退出了。

那么exitmainreturn有什么不同呢?其实以上案例已经很好地说明了:

exit不论在哪一个函数中执行,都是直接终止整个进程

但是exit其数有两个版本,一个是<unistd.h>中的_exit

在这里插入图片描述

另外一个是<stdlib.h>中的exit

在这里插入图片描述

可以看出,_exitman的第二页,是系统调用接口,而exitman的第三页,是用户操作接口。但是两者的函数几乎完全一致,都是void exit(int status),因此两者效果几乎完全一致。

除了操作系统层面不同,另外的区别就是:exit会刷新缓冲区,而_exit不会刷新缓冲区

毫无疑问的是,_exit是系统调用接口,更接近底层,exit中一定封装了_exit+刷新缓冲区两个功能。而在其它的操作系统中,终止进程的接口不一定是_exit,因此exit在不同操作系统中封装的接口大概率是不一致的。


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】深入理解KMP算法及C语言实现

一、KMP算法简介 KMP算法&#xff08;Knuth-Morris-Pratt算法&#xff09;是一种高效的字符串匹配算法&#xff0c;由Donald Knuth、James H. Morris和 Vaughan Pratt共同发明。KMP算法的核心思想是当一次字符比较失败时&#xff0c;利用已经得到的部分匹配信息&#xff0c;将模…

JVM虚拟机监控及性能调优实战

目录 jvisualvm介绍 1. jvisualvm是JDK自带的可以远程监控内存&#xff0c;跟踪垃圾回收&#xff0c;执行时内存&#xff0c;CPU/线程分析&#xff0c;生成堆快照等的工具。 2. jvisualvm是从JDK1.6开始被继承到JDK中的。jvisualvm使用 jvisualvm监控远程服务器 开启远程监控…

【Java框架】SpringMVC(三)——异常处理,拦截器,文件上传,SSM整合

目录 异常处理解释局部异常处理全局异常 拦截器拦截器介绍作用:拦截器和过滤器之间的区别拦截器执行流程代码实现补充 文件上传依赖配置MultipartResolver编写文件上传表单页APIMultipartFileFile.separator必须对上传文件进行重命名代码示例 SpringMVC文件上传流程多文件上传 …

mybatis中<if>条件判断带数字的字符串失效问题

文章目录 一、项目背景二、真实错误原因说明三、解决方案3.1针对纯数字的字符串值场景3.2针对单个字符的字符串值场景 四、参考文献 一、项目背景 MySQL数据库使用Mybatis查询拼接select语句中进行<if>条件拼接的时候&#xff0c;发现带数字的或者带单个字母的字符串失效…

Coursera: An Introduction to American Law 学习笔记 Week 03: Property Law

An Introduction to American Law 本文是 https://www.coursera.org/programs/career-training-for-nevadans-k7yhc/learn/american-law 这门课的学习笔记。 文章目录 An Introduction to American LawInstructors Week 03: Property LawKey Property Law TermsSupplemental Re…

LM324的输出VOL与IOL你注意过吗?

电路图 途中LMC6084 更改为LM324 故障现象 这个电路的输入输出表达式为 R30 两端电压等于0V 当J16 的4脚与2脚相等&#xff0c;等于5V&#xff08;或者4脚略大于2脚时&#xff09;7脚输出 约 500mV&#xff1b; 实际应该为0V左右才对.见下图 故障原因 上图运放输出低电平…

AI重塑数字安全,安恒信息行胜于言

有人曾言&#xff1a;所有行业都值得基于人工智能技术重做一遍。 深以为然。如今&#xff0c;数字安全产业面临着一次重要的重塑机遇。以大模型为代表的人工智能技术正深刻影响着数字安全市场格局、产品研发、技术方案以及运营服务。产业界已形成共识&#xff0c;即谁能抓住人…

Nginx+Lua+OpenResty(详解及使用)

一、 Nginx简介 Nginx是一个高性能的Web服务器和反向代理的软件。 Web服务器&#xff1a;就是运行我们web服务的容器&#xff0c;提供web功能&#xff0c;还有tomcat也提供类似的功能。 代理是软件架构和网络设计中&#xff0c;非常重要的一个概念。 二、Nginx的反向代理&…

WEB服务的配置与使用 Apache HTTPD

服务端&#xff1a;服务器将发送由状态代码和可选的响应正文组成的 响应 。状态代码指示请求是否成功&#xff0c;如果不成功&#xff0c;则指示存在哪种错误情况。这告诉客户端应该如何处理响应。较为流星的web服务器程序有&#xff1a; Apache HTTP Server 、 Nginx 客户端&a…

百度网盘svip白嫖永久手机2024最新教程

百度网盘&#xff08;原名百度云&#xff09;是百度推出的一项云存储服务&#xff0c;已覆盖主流PC和手机操作系统&#xff0c;包含Web版、Windows版、Mac版、Android版、iPhone版和Windows Phone版。用户将可以轻松将自己的文件上传到网盘上&#xff0c;并可跨终端随时随地查看…

爬虫抓取网站数据

Fiddler 配置fiddler工具结合浏览器插件 配置fiddler Tools--Options 抓包技巧 谷歌浏览器开启无痕浏览,使用SwitchyOmega配置好代理端口 Ctrl x 清理所有请求记录,可以删除指定不需要日志方便观察 设置按请求顺序 观察cookie,观察请求hesder cookie和row返回结果 Swit…

《QT实用小工具·四十二》圆形发光图像

1、概述 源码放在文章末尾 该项目实现了图像的发光效果&#xff0c;特别适合做头像&#xff0c;项目demo演示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; import QtQuick 2.7 import QtGraphicalEffects 1.12Item {id: rootwidth: 80height: 80property int ra…

写Python需要养成的9个编程好习惯

以写Python代码为例&#xff0c;有以下9个编程好习惯。 1. 提前设计 写代码和写作文一样&#xff0c;需要有大纲&#xff0c;不然很容易变成"屎山"。 思考业务逻辑和代码流程&#xff0c;是动手前的准备工作&#xff0c;这上面可以花一半以上时间。 一些程序员洋…

【考研数学】全程跟张宇,《1000题》应该怎么搭配《660》《880》?

数一125学长来了&#xff0c;选题集和选老师经验满满&#xff0c;手把手教你避雷&#xff0c;直接不废话&#xff0c;三本题集很重要&#xff0c;筛选找重点事半功倍。 就老师而言&#xff0c;如果已经决定全程跟张宇老师了&#xff0c;那么就不要中途换老师&#xff01; 就题…

按照模板导出复杂样式的excel

导出excel通常使用的是apache poi,但是poi的api相当复杂&#xff0c;所以当导出的excel样式比较复杂时&#xff0c;写起来就比较头疼了&#xff0c;这里推荐使用easypoi, 可以很方便的根据模板来导出复杂excel 文档地址: 1.1 介绍 - Powered by MinDoc 我们要实现如图所示效果…

C++的二叉搜索树

目录 基本概念 二叉搜索树的实现 插入结点 查找结点 删除结点 删除结点左为空 删除结点右为空 基于特殊情况的优化 删除结点左右不为空 基于特殊情况的优化 完整代码 二叉搜索树的实际应用 K和KV模型 改造二叉搜索树为为KV模型 基本概念 1、二叉搜索树又称二叉…

数据结构之二叉搜索树底层实现洞若观火!

目录 题外话 正题 二叉搜索树 底层实现 二叉搜索树查找操作 查找操作思路 查找代码实现详解 二叉搜索树插入操作 插入操作思路 插入代码详解 二叉搜索树删除操作 删除操作思路 删除代码详解 小结 题外话 我的一切都是党给的,都是人民给的,都是家人们给的!! 十分感…

LeetCode:51. N 皇后

leetCode51.N皇后 题解分析 代码 class Solution { public:int n;vector<vector<string>> ans;vector<string> path;vector<bool> col, dg,udg;vector<vector<string>> solveNQueens(int _n) {n _n;col vector<bool> (n);dg …

通过matlab对比遗传算法优化前后染色体的变化情况

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 通过matlab对比遗传算法优化前后染色体的变化情况. 2.测试软件版本以及运行结果展示 MATLAB2022A版本运行 3.核心程序 ....................................…

AI视频教程下载:用ChatGPT和 MERN 堆栈构建 SAAS 项目

这是一个关于 掌握ChatGPT 开发应用的全面课程&#xff0c;它将带领你进入 AI 驱动的 SAAS 项目的沉浸式世界。该课程旨在使你具备使用动态的 MERN 堆栈和无缝的 Stripe 集成来构建强大的 SAAS 平台所需的技能。 你将探索打造智能解决方案的艺术&#xff0c;深入研究 ChatGPT 的…