Linux:进程创建 进程终止

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.mzph.cn/web/3358.shtml

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

相关文章

个人博客系统的设计与实现

https://download.csdn.net/download/liuhaikang/89222885http://点击下载源码和论文 本 科 毕 业 设 计&#xff08;论文&#xff09; 题 目&#xff1a;个人博客系统的设计与实现 专题题目&#xff1a; 本 科 毕 业 设 计&#xff08;论文&#xff09;任 务 书 题 …

算法-动态规划专题

文章目录 前言 : 动态规划简述1 . 斐波那契模型1.1 泰波那契数列1.2 最小花费爬楼梯1.3 解码方法 前言 : 动态规划简述 动态规划在当前我们的理解下,其实就是一种变相的递归,我们查看一些资料也可以知道,动态规划其实属于递归的一个分支,通过把递归问题开辟的栈帧通过一定的手…

“亚马逊依赖”之下,傲基科技的品牌势能如何提升?

受益于出口政策红利、低人工成本、完善的供应链以及成熟的生产工艺优势&#xff0c;近年来我国家具出口行业迅速发展。 数据显示&#xff0c;我国家具出口规模1995年仅为11.06亿美元&#xff0c;至2023年增至641.96亿美元。随着出口规模持续扩大&#xff0c;相关企业积极走入公…

【OpenGL实践08】现代渲染管线在GLUT和Pygame和Qt.QOpenGLWidget上各自的实现代码

Qt.QOpenGLWidget进行现代渲染管线实验效果 一、说明 据说QOpenGLWidget是用来取代QGLWidget的继承者&#xff0c;我们试图将GLUT上的旧代码改成QOpenGLWidget&#xff0c;本以为差别不大&#xff0c;轻易搞定&#xff0c;经实践发现要付出极大努力才能完成。经多次实验发现G…

编译原理实验课

本人没咋学编译原理&#xff0c;能力有限&#xff0c;写的不好轻点喷&#xff0c;大佬路过的话&#xff0c;那你就路过就好 东大编译原理实验课原题&#xff0c;22年 1. 基本题&#xff1a;简单的扫描器设计 【问题描述】 熟悉并实现一个简单的扫描器&#xff0c;设计扫描器…

nosql数据库 redis

一、介绍 1、redis与mysql的区别&#xff1a; Redis是一种基于键值对的内存数据库&#xff0c;数据存储在内存中&#xff0c;因此读写速度非常快。它支持多种数据结构&#xff0c;如字符串、哈希、列表等。 MySQL是一种关系型数据库&#xff0c;数据以表格的形式组织存储在磁…

探索 Python 的动态类型系统:变量引用、不可变性及高效内存管理与垃圾回收机制的深入分析

文章目录 1. 动态类型及其内存管理解析1.1 变量与对象的引用关系1.2 对象的不可变性和内存地址的变化 2. 垃圾回收与内存优化策略2.1 动态内存分配的基础2.2 Python 的垃圾回收 Python作为一种流行的高级编程语言&#xff0c;以其代码的易读性和简洁性著称。尤其是它的动态类型…

泛私域新引擎:小程序AI智能名片S2B2C商城的分销式导购策略与案例剖析

在数字化浪潮的推动下&#xff0c;小程序AI智能名片S2B2C商城以其独特的分销式导购能力&#xff0c;逐渐在泛私域的特殊场景中占据了一席之地。这种模式不仅打破了传统线上线下的界限&#xff0c;更通过以人为核心的营销方式&#xff0c;实现了利益驱动的深度分销。 分销式导购…

Git学习笔记(四)远程仓库

根据前面几篇文章的介绍&#xff0c;在本地使用Git基本不成问题了&#xff0c;常用的基本命令和一些基本概念基本也介绍完毕了。这一张主要讲讲远程仓库的创建和使用。 概念 其实在前面第一篇文章中&#xff0c;我们就简单介绍过远程仓库&#xff0c;它其实就是一个托管在远程服…

yolo-驾驶行为监测:驾驶分心检测-抽烟打电话检测

在现代交通环境中&#xff0c;随着汽车技术的不断进步和智能驾驶辅助系统的普及&#xff0c;驾驶安全成为了公众关注的焦点之一 。 分心驾驶&#xff0c;尤其是抽烟、打电话等行为&#xff0c;是导致交通事故频发的重要因素。为了解决这一问题&#xff0c;研究人员和工程师们…

19-ESP32-S3外设IIC

ESP32-S3的IIC 引言 ESP32-S3是一款集成了Wi-Fi和蓝牙功能的低成本、多功能微控制器。在这篇博客中&#xff0c;我们将详细介绍ESP32-S3的IIC&#xff08;Inter-Integrated Circuit&#xff09;接口&#xff0c;也被称为I2C。 IIC简介 IIC是一种串行、同步、多设备、半双工…

CTF网络安全大赛详情

网络安全已成为现代社会的一个关键挑战&#xff0c;随着互联网技术的飞速发展&#xff0c;从个人隐私保护到国家安全&#xff0c;网络安全的重要性日益突显。为了应对这一挑战&#xff0c;CTF&#xff08;Capture The Flag&#xff0c;中文&#xff1a;夺旗赛&#xff09;应运而…

openEuler-22.03下载、安装

一、下载 下载地址&#xff1a;openEuler下载 | 欧拉系统ISO镜像 | openEuler社区官网 下载版本&#xff1a;openEuler-22.03-LTS-x86_64-dvd.iso 二、安装 配置完后开启虚拟机 设置完后&#xff0c;重启虚拟机 设置桥接模式的网络 cd /etc/sysconfig/network-scripts/ vi if…

LeetCode 2739. 总行驶距离

题目链接https://leetcode.cn/problems/total-distance-traveled/?envTypedaily-question&envId2024-04-25 简单题&#xff0c;看代码思考一下即可理解 class Solution {public int distanceTraveled(int mainTank, int additionalTank) {int res 0;while (mainTank >…

RK3568 学习笔记 : u-boot 千兆网络无法 ping 通PC问题的解决

前言 开发板型号&#xff1a; 【正点原子】 的 RK3568 开发板 使用 虚拟机 ubuntu 20.04 收到单独 编译 RK3568 u-boot 【问题】u-boot 千兆网络无法ping 通&#xff1f;Linux 下千兆网络正常&#xff0c;说明&#xff1a;开发板硬件正常 u-boot 下网络如果通了&#xff0c;…

【高频】基于GBDT-FM模型的level-2高频数据实证研究(二)

【高频】基于GBDT-FM模型的level-2高频数据实证研究&#xff08;二&#xff09; 原创 Yud. 2AMquant 2024-04-04 11:30 广东 上一篇中初步提及了Level2数据中常见变量指标的构建方式&#xff0c;以及其带来的价格冲击。此篇将使用GBDT-LM模型对短程价格走势进行简单预测。 ps…

Nginx下载安装,什么是nginx,什么是反向代理,Windows下、linux下安装nginx(保姆级教程)

文章目录 一、Nginx简介为什么要使用NginxNginx的特点Nginx的相关概念正向代理反向代理动静分离负载均衡 二、Nginx安装1. Windows安装2. Linux安装 一、Nginx简介 Nginx 是一个高性能的 HTTP&#xff08;静态资源服务器&#xff09; 和 反向代理 Web 服务器。 为什么要使用N…

AI-数学-高中-44导数的运算法则

原作者视频&#xff1a;【导数】【一数辞典】3导数的运算法则&#xff08;略难&#xff09;_哔哩哔哩_bilibili 三种求导表达方式一样的&#xff0c;中间的比较常用&#xff1a; 链式法则&#xff1a;从外向内&#xff1a;

如何部署Tensorrtx

因为模型跑起来太慢了&#xff0c;所以想要运用tensorrtx进行加速。但是这个是有难度的&#xff0c;且网络上的教程大多写的不是很好。我将以一个新人的视角&#xff0c;从头开始部署基于yolov5的tensorrtx加速。 知识补充&#xff1a;TensorRT和TensorRTX的区别 tensorRT是英…

统一SQL 支持Oracle cast函数转换

统一SQL介绍 https://www.light-pg.com/docs/LTSQL/current/index.html 源和目标 源数据库&#xff1a;Oracle 目标数据库&#xff1a;Postgresql&#xff0c;TDSQL-MySQL&#xff0c;达梦8&#xff0c;LightDB-Oracle 操作目标 在Oracle中&#xff0c;cast函数允许将一种…