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 解码方法 前言 : 动态规划简述 动态规划在当前我们的理解下,其实就是一种变相的递归,我们查看一些资料也可以知道,动态规划其实属于递归的一个分支,通过把递归问题开辟的栈帧通过一定的手…

1002 - 编程求解1+2+3+...+n

题目描述 编程求解下列式子的值&#xff1a; S123 \dots nS123⋯n。 输入 输入一行&#xff0c;只有一个整数 n(1 \le n \le 1000)n(1≤n≤1000) 。 输出 输出只有一行&#xff08;这意味着末尾有一个回车符号&#xff09;&#xff0c;包括 11 个整数。 样例 输入 100 …

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

受益于出口政策红利、低人工成本、完善的供应链以及成熟的生产工艺优势&#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…

【postgresql初级使用】updatable view 可修改的视图,以及视图数据致性的控制,完全分离数据报表业务与数据的维护操作部署架构尝试

可修改的视图 ​专栏内容: postgresql使用入门基础手写数据库toadb并发编程个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. 文章目录 可修改的视图概述 可修改视图介绍

编译原理实验课

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

Spring从零开始学使用系列(三)--依赖注入(DI)

目录 1.DI的核心概念 1.1优势 2. Spring中的DI实现 2.1 构造器注入 2.1.2 优势和缺点 2.2 设置器注入 2.2.1 如何使用设置器注入 2.2.2 示例代码 2.2.3优势和使用场景 2.3 字段注入 2.4 方法注入 2.4.1 方法注入的概念 2.4.2 找方法注入 2.4.3 Lookup 注解的作用 2…

nosql数据库 redis

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

【Python】使用Pandas和随机森林对鸢尾花数据集进行分类

我在鼓楼的夜色中 为你唱花香自来 在别处 沉默相遇和期待 飞机飞过 车水马龙的城市 千里之外 不离开 把所有的春天 都揉进了一个清晨 把所有停不下的言语变成秘密 关上了门 莫名的情愫啊 请问 谁来将它带走呢 只好把岁月化成歌 留在山河 &#x1f3b5; 鹿…

鸿蒙原生应用元服务-访问控制(权限)开发应用权限列表二

ohos.permission.ACCELEROMETER 允许应用读取加速度传感器的数据。 权限级别 &#xff1a;normal 授权方式 &#xff1a;system_grant ACL使能 &#xff1a;TRUE ohos.permission.GYROSCOPE 允许应用读取陀螺仪传感器的数据。 权限级别 &#xff1a;normal 授权方式 &a…

探索 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是一种串行、同步、多设备、半双工…

Python中, 介绍面向对象及三大特征

面向对象(Object Oriented Programming) OOP, 是一个重要的编程范式, 它主要基于"对象"的概念, 这些对象具有属性(数据)和方法(函数) 三个特征: 封装, 继承,多态 简单来说: 封装: 隐藏内部实现细节, 只保留功能接口 继承: 类与类之间的关系 (比如说父和子的关系…

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 >…