news 2026/4/7 0:12:52

进程(9)虚拟内存part1补充,进程创建,进程终止,进程等待

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进程(9)虚拟内存part1补充,进程创建,进程终止,进程等待

本章目标

1.虚拟内存补充
2.如何实现写时拷贝(进程创建)
3.进程终止
4.进程等待

1.虚拟内存补充

1.1为什么要有虚拟内存

在前面的章节我们讨论过,什么是虚拟内存,虚拟内存的结构.但是,这样一个我们为什么要去使用虚拟内存,为什么不去直接使用物理内存呢.
如果计算机技术放在四五十年前,我们的os确实是直接使用物理内存.
1.但是这样做是有风险的如果一个病毒对一个地址直接能够通过指针去修改物理内存,可能会让设备直接瘫痪.
2.第二点这样做我们没有办法确定地址.我们知道一个程序编译好了之后,它会将自己的编译好的程序放在磁盘当中.但是我们将一个程序加载的时候,我们是无法确定现在物理内存具体是使用到了哪里的第⼀次执⾏a.out时候,内存当中⼀个进程都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了.
3.第三点这样做的时候效率低下,我们使用虚拟内存最大的好处是可以让磁盘中的数据可以任意的加载物理内存当中,我们可以通过页表直接找到程序具体在什么位置,如果使用物理内存就相当于将整个进程当作一个整体,我们就需要给它开一个具体大小的空间了.如果这个进程是32位的话,我们就要内存当给它开一个4个g的空间,但是这么做,效率是相当低的.我们如果一个程序没有调用,需要将它挂起到swap分区,就需要将进程整个过程拷走.这相当于与磁盘进行io这个效率是相当低的.
而虚拟内存的好处就是解决这几个问题的
1.使用虚拟内存能够控制进程的行为,拦截进程的非法操作.
因为使用进程的时候,我们是使用页表将虚拟地址转化为物理地址的.
如果进程有非法的操作,我们直接让页表不对其进行转化.
2.我们能够将进程管理和内存管理进行解耦合,
在当代操作系统,我们是将物理地址直接隐藏不暴露给用户的
我们通过页表的这样的机制,让虚拟地址和物理地址联系起来
这样操作,我们申请空间的时候new或者malloc出来的空间都是申请的虚拟地址.相当于申请的是虚拟地址空间的堆区
当我们的程序真正的需要这些空间的时候,再通过类似与缺页中断的方式,重新对资源进行二次申请.物理内存那边再通过特定的算法对其进行申请资源,将地址重新填回页表
3.我们能够将无序变为有序

我们虚拟地址空间的空间布局永远都是这样的,我们我们通过页表的映射.可以让物理内存的地址可以任意的加载到物理地址空间的任意位置.

1.2vm_area_struct

在前面我们说过在Linux操作系统当中对虚拟地址空间进行管理的是mm_struct,但是在虚拟地址空间为了保证其下属的其他的结构不受影响相互独立,就仍然需要一个结构对其进行管理,将堆区栈区等等进行分块进行管理

实际的情况是这样的.
这样做的一个好处就是能够处理堆区,我们知道堆区相对于其他区域是离散的,他可能由多个区域构成,我们可以通过多个vm_area_struct将其连结起来,这样我们需要对某一个具体的堆区进行操作,就可以进行对特定的vm_area_struct进行操作了.
我们现在找一下vm_area_struct的源码

进程的虚拟空间区域组织方式一般有两种
1,当虚拟内存区域较少的时候使用mmap进行管理,这是一个链表.
2,当虚拟内存区域多的时候,使用红黑树进行管理.
二者的本质都是管理vma,我们在这里只介绍一种vm_area_struct

这里面有两个指针vm_start和vm_end,分别指向这块vma的开始与结束.
这个方法与在mm_struct的结构是类似的,但是这个描述的具体区域那个更像一个总纲描述是整个虚拟地址空间的大小.
在这里还有一个标志位和文件的映射的问题,我们后面单独写一篇文章介绍.

虚拟地址空间的具体结构.我们通过mmap这个指针可以遍历这个链表找到不同的区域.
这里面除了堆区以外因为是离散的有多个vm_area_struct,其他的一般只有一个vm_area_struct进行管理.

2.如何实现写时拷贝(进程创建)

2.1如何实现写时拷贝

有关于进程拷贝,在前面我们已经讨论很多了.主要就是关于fork这个系统调用.关于它的使用和返回值.最后一个为什么一个变量可以接受两个返回值我们在前面也已经讨论完了.
在当时我们说的是,当子进程对其数据进行修改的时候,会进行写时拷贝,但是我们没有提如何进行写时拷贝.而在这一小节我们要补充的就这一个.

我们说过我们一个进程在创建的时候是通过父进程通过fork以自身为模板创建子进程的从而在内存当中多出来一个进程.而在创建这个过程当中,父进程会进程进程的代码和数据在页表当中将他们的标志位,权限的那个从rw改为只读.这样子进程拿到的页表也就是只读的.
当子进程或者父进程对其数据进行修改的时候,这样os就能够知道要进行写实拷贝了.
具体实现是这样的,因为在没修改前这两个进程的页表都是只读,这两个页表的地址也都在内存中存在并没有被挂起.具体体现在页表当中就是那个存在的标志位不为0.这样os就知道这连两个页表指向同一块物理空间.因为一方要对其进行修改.我们的os就在物理内存重新申请一块空间,将他们填回到修改的进程的页表当中.这样就完成了写实拷贝.

2.2fork失败的原因

fork失败的原因主要是两个方面
1.fork的时候进程太多了.2进程的数量过多超过了用户的限制.

3.进程终止

我们在前面说过一个进程想要进行创建是,要先创建内核数据结构,然后在需要进行调度它的时候,我们在进行加载,这个过程被我们叫做懒加载,但是当一个进程要进行终止的时候要做的就是释放其资源和数据,然后再去释放内核数据结构,具体表现这个进程变为僵尸进程了.
一个进程有下面三种情况
1.代码跑完了,结果正常.
2.代码跑完了,结果不正常
3.代码没跑完,异常终止了.
一个进程退出的时候它的信息有两个部分组成信号和退出码.
信号我们放到进程等待的时候介绍.
我们先解决前两种.
在我们之前在一个进程结束前会通过return 0的时候表示程序结束.
0表示正常.而从1到255表示进程出错.但是这种错误吗,我们人是不喜欢这种方式.我们通过宏的方式去定义这个错误与正常.
一般来说我们大部分的程序是没有日志的,我们只能够通过这个退出码去表示程序是否正常执行了.
而这个错误码我们一般是放在errno,这个外部变量当中的,strerror这个函数进行转化我们去拿到对应的错误信息.我们现在简单测试一下

#include<stdio.h>#include<errno.h>#include<string.h>intmain(){inti=0;for(;i<=255;i++){printf("%d->%s\n",i,strerror(i));}return0;}

因为我们不是很在乎这个main函数的退出码,我们可以任意设置

我们将它改为3,然后重新编译,我们可以通过$?去查看一个最近进程的退出码,

我们过去也在一个函数中进行return,函数又被叫做子程序,函数的末尾表示的是函数的结束,而在main函数当中就是该进程的退出码.
在之前我们也用过exit这个函数,这个函数如果放在之前是与return等价的,但是在这里我们引出它的新用法

#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<stdlib.h>voidtest(){printf("我是子进程,我要退出了\n");exit(0);}intmain(){pid_tid=fork();if(id==0){intcnt=5;while(cnt--){printf("我是子进程的pid是%d\n",getpid());sleep(1);}test();}else{while(1){printf("我是父进程我的pid%d\n",getpid());sleep(1);}}return0;}



我们看到子进程在执行完test函数之后直接退出了.我们可以看出这个东西的作用在多执行流当中可以让进程退出的.
但是关于exit这个函数是一个库函数,它并不是真正的系统调用.
它底层调用的是_exit这个函数.
我们可以看一下手册


我们可以修改一下代码


我们把日志的换行删除了,并修改了成为系统调用,二者的一个区别就在于,我们看到日志没被打印.这就是说我们的系统调用并不会刷新缓冲区.
这也就侧面的说明了一个问题.
我们的库函数是对系统调用的二次开发.
这个缓冲区是c语言在替我们维护.是语言级别的缓冲区.

4.进程等待

在前面我们说过一个进程变为僵尸之后,最大的意义是为了回收资源拿到他的退出信息,来查看是否成功完成任务.如果一个僵尸没有被回收就会造成内存资源的泄漏.我们即使是通过发送kill -9这样的信号也没办法杀死它.无法杀死一个已经死掉的进程
在Linux当中是通过进程等待的方式进行回收子进程的.
创建一个进程是fork系统调用.那么回收进程自然也就是通过系统调用来实现的

在Linux当中常见的进程回收有很多,我们介绍两种,我们介绍先介绍一个最简单的.
wait,这个函数的返回值一共有两种,成功返回子进程的pid,失败返回-1,在errno中设置错误码.
它的参数是子进程的退出状态,这是一个输出型的参数,我们现在不关系.我们在后面再具体介绍,如果我们不需要这个参数,我们就给一个null就可以了.
这个wait它是等待任意一个子进程退出的.它是能够知道子进程是否退出了的

#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#include<stdlib.h>intmain(){pid_tid=fork();if(id==0){intcnt=5;while(cnt--){printf("son process %d\n",getpid());sleep(1);}exit(0);}else{sleep(10);intrid=wait(NULL);if(rid==id){printf("wait success \n");sleep(5);}else{printf("wait no\n");}}return0;}

在这里我们能够先让子进程退出,同时让父进程休眠10秒,为了能够看到子进程进入僵尸状态.回收之后只有父进程一个,等待5秒父进程退出

我们看到了结果和我们想的一样.关于waitpid我们放到下一个文章和程序替换一起说

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/3 2:06:32

企业CI/CD中处理Git认证错误的实战指南

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个演示项目&#xff0c;模拟CI/CD管道中出现的Git认证错误场景。包含&#xff1a;1. 故意配置错误的Git凭据&#xff1b;2. 展示日志中REMOTE: INVALID USERNAME OR TOKEN错…

作者头像 李华
网站建设 2026/3/26 18:27:08

2026年,全网亲测有效的10款降ai神器盘点!(持续更新)

最近好多同学在后台问我&#xff0c;论文查重红了一片怎么办。其实呢&#xff0c;今年学校查得严&#xff0c;不仅查复制比&#xff0c;还要查AIGC。说白了&#xff0c;就是看你有没有用AI写。我自己试了一圈&#xff0c;发现降低ai率真是一门玄学。 有些同学为了免费降低ai率&…

作者头像 李华
网站建设 2026/3/26 20:59:38

零基础教程:5分钟用快马创建你的第一个DOWNKYI下载器

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个最简单的DOWNKYI单视频下载器GUI应用&#xff0c;要求&#xff1a;1) 使用PySimpleGUI构建界面 2) 输入B站视频URL即可下载 3) 提供清晰的状态提示 4) 适合完全不懂编程的…

作者头像 李华
网站建设 2026/4/2 0:10:37

【计算机毕业设计案例】基于python深度学习的乐器识别卷神经网络

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/30 13:41:02

1小时搭建Redis面试Demo:6大考点可视化展示

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 构建一个Redis知识可视化演示系统原型&#xff0c;要求&#xff1a;1.6个独立模块分别展示数据结构、持久化等核心概念2.实时数据流动动画&#xff08;如RDB快照过程&#xff09;3…

作者头像 李华