进程的概念
目录
一.前提
1.冯诺依曼体系结构
日常生活中,我们所使用的计算机手机大多都遵循冯诺依慢体系结构

输⼊单元:包括键盘, ⿏标,话筒,摄像头扫描仪, 写板,网卡,磁盘(外存)等
中央处理器(CPU):运算器+控制器
储存器:内存
输出单元:显示器,磁盘,网卡,打印机等
软件运行,实则是CPU访问我们的代码,而我们的代码加载到CPU实则是一个数据从一个设备拷贝到另一个设备上,故整个体系结构的效率由设备的拷贝效率决定,那么在程序运行之前,这段数据会在磁盘的文件夹中存储,整个外设在操作系统眼中就是不同的文件夹,数据在不同文件夹进行增删查改的操作
外设输入数据时先输入到内存,再由CPU访问内存进行运行,那么为什么外设不直接将数据传到CPU而是先放到内存呢?

由图可知,越靠近CPU的操作设备成本越高,而内存是成本低效率还行(性价比高)的容器,我们如果直接让外设将数据输入到CPU中,他的传输效率很低,导致整体系统运行效率低下,故要将输入的数据预存到内存中,这样保证了整体的运行效率
2.操作系统
2.1.概念
操作系统是一个管理软硬件的软件,包括(狭义上)内核(进程管理,内存管理,文件管理,驱动管理)和(广义上)其他程序(函数库,shell程序等等)

2.2.OS(操作系统)的目的
对下,与硬件交互,管理所有的软硬件资源
对上,为用户程序(应⽤程序)提供⼀个良好的执行环境

2.3.核心作用
在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的“搞管理”的软件
如何理解“管理”,这里我们举个学校中校长管理学生的例子

校长管理学生不需要亲自见面,可以通过辅导员来间接管理学生,辅导员将每个学生的数据(描述)以结构体形式填写完成,最后交给校长,校长只要对学生的数据进行管理即可,再将这些学生的数据通过链表的形式组织起来进行管理


故计算机管理硬件:先描述再组织
1.描述起来,⽤struct结构体
2. 组织起来,⽤链表或其他高效的数据结构
2.4.系统调用
操作系统在对我们用户提供对应的服务时,操作系统是不信任任何用户的,所以操作系统通过它给你包装好的库函数接口,只需要使用它给你包装好的库函数实现对应的功能,这个过程称作系统调用。

二.进程
那么操作系统是如何对进程进行管理的呢,联系上文我们可知,操作系统是先把进程先描述(struct结构体)再组织(链表)
1.基本概念
1.1.描述进程-PCB
进程=PCB(task_ struct)+自己的代码和数据
类似于面试过程中(进程)=个人简历(PCB)+个人能力
1.2.task_ struct
进程的所有属性,都可以在task_ struct上直接或间接找到,下面是task内包含的内容

而不同的进程(PCB)是通过双链表的形式链接起来的
1.3.查询进程
我们历史上执行过的指令,工具以及自己的程序,都是进程,那么我们如何查询我们的进程呢?

我们可通过getpid()来获取进程id以及getppid()获取其父进程的id,通过id

我们可通过ps ajx | head -1;ps ajx |grep myprocess(分号前是查询进程列表表头,分号后是查询对应的程序进程,分号表示和,也就是这两条指令一起执行)指令来查询自己的进程

此时我们查询我们自己程序的进程时,下面grep自己也是个进程,所以下面也会它对应的进程状态
我们可通过ctrl+c/kill -9(9是信号编号) (进程id)来杀死进程
补充:
我们可以通过proc/进程id 来展现进程的所有属性

1.如果我们删除了下面路径的exe文件,那我们的程序还会跑吗?
答案是会的,我们在基本概念的图中可知,我们此时删除的是磁盘中的文件,而它已经在内存中
拷贝了,此时它运行的程序是内存中拷贝的程序,若停止运行再次运行就运行不了了(exe)
2.cwd:保存该程序所在的一个当前路径,此时进程生成就在当前路径加个进程名就是进程的路径
故我们可通过在.c文件中使用chdir(路径)来改变当前路径来改变进程的生成路径
1.4.fork函数

父进程是19811是一个bash,他是一个命令行解释器,本质是一个进程,命令行就是你最下面待输入的地方,我们执行的所有命令,他们都包含同一个父进程bash,那么我们应该如何证明呢?

我们有一个fork函数,可以创建子进程,正常一个main函数只有一个执行流(进程),当经过fork函数时,会产生两个执行流也就是说fork下面的printf函数会跑两次

由于子进程被创建,它没有自己的代码和数据,它会共享父进程的代码数据(fork()执行父进程的printf()数据)只是将自己的pid()更改了,子进程的PCB中代码数据指向的还是父进程的数据和代码


此时fork函数之前的main函数进程id为1620,父进程为前面提到的-bash为19811,fork后产生的进程id为1621,父进程为main函数的进程为1620,这也验证了我们上面的说法(父子进程的代码是共享的),fork函数有两个返回值(可以通过不同返回值实现父子进程的不同功能)
那么为什么fork函数会返回两次?

一个函数在执行return之前,它的核心功能已经完成了,也就是说fork函数在return之前,子进程就已经创建好并且被调度了,而return的本质其实就是语句,它也是代码也会拷贝到子进程中,故会返回两次(后面的虚拟地址会讲到),在Linux中父进程:子进程=1:n
那如果父子进程中在更改了一个他们两个共享变量的值,父子进程该如何变化呢?此时增加了一个变量gval值为100,子进程更改并打印gval的值,父进程只打印gval的值


结果可知,子进程更改了gval的值,而父进程没有被修改,这说明了父子进程是相互独立的,子进程修改数据通过写时拷贝到磁盘中(再拷贝一份),故不会影响到父进程(更详细的后面虚拟地址会讲)
2.进程状态
顾名思义就是一个进程处于不同的状态,进程状态是task_struct中的一个整形变量


在一个CPU中,专门给PCB设置了一个调度队列,所以一个PCB,它即可一属于双链表(a数据结构),又可以属于调度队列(b数据结构)

操作系统中不只有运行队列(runqueue(task_struct*))还有设备队列(struct device*)等等
2.1.进程状态详解
运行:进程在调度队列中,它的状态就是running
阻塞:等待某个设备或者资源就绪:OS管理各种资源((struct device):键盘,显示器,网卡,磁盘...),先描述,再组织!
从运行到阻塞:将task_struct调度到各个设备(struct device)的等待队列(wait queue)中(如C语言中scanf()函数就是将进程调度到键盘的等待队列中,等待键盘输入就绪)
再从阻塞到运行:当键盘按下时,操作系统第一个知道,操作系统检测键盘的等待队列是否为空,再将该状态设置为活跃状态,再将该进程重新运回到运行队列中
故进程状态的变化:就是PCB在不同队列中进行流动,本质就是数据结构的增删查改
挂起:当操作系统在内存资源严重不足时,有一些数据它在没有就绪还占有内存资源的情况下,磁盘中存在一个swap交换分区,此时操作系统将这些代码数据唤出,到swap分区中,只保留PCB,这称为阻塞挂起(将没有就绪的资源挂到外设上);当这个数据准备就绪时,操作系统会再将它重新构建内存,重新指针映射,将其重新唤入到内存中来,CPU中的资源若调度到外设中还是不够,它甚至会将运行队列末端的部分数据唤出到磁盘中去,这称为运行挂起
2.2.理解内核链表
笼统的双链表,next和prev指针指向的是整个Node节点地址

内核链表,它会创建一个list_head成员,这个成员中只包含head和prev的数据,也就是说它的head和prev指针指向的是下一个节点的next和prev的地址

我们如何访问这个内核链表的其他属性呢?在结构体中结构体本身地址=结构体第一个成员的地址,结构体内部成员的地址是依次增大的,所以我们只需要找到第一个内部成员的地址(struct task_struct*)0(在0号地址处存放一个结构体),然后访问它的list_head links变量的地址&((struct task_struct*)0->links),这样我们就可以得到0号地址到links变量的偏移量,然后我们只需要再将next-&((struct task_struct*)0->links)就能访问到这个结构体的头地址再进行强转之后就能访问其他数据了(strucr task_struct*)(next-&((struct task_struct*)0->links))
而我们的PCB中可以存在多个的list_head节点,PCB通过队列来管理一个个数据节点的指针,数据节点指针再通过链表的形式联系到一起

这样我们就理解了为什么PCB(一个PCB只存在一份)即在运行队列里,又在双链表里
2.3.进程状态的查看

例如我们在查询进程中使用到的(ps ajx | head -1;ps ajx |grep myprocess)

R:运行状态,S:阻塞状态(浅度/可中断休眠)
T:暂停状态(CTRL+Z用户通过操作让系统暂停或者kill -19(后面信号会讲) 进程id),t:相当于代码中打断点使得代码暂停(常用于debug调试)
暂停和阻塞有什么区别呢?
阻塞往往是等待某个设备就绪
暂停是代码中某个条件不具备,操作系统就把你的系统暂停了(常用于操作系统怀疑这个程序有问题但是问题不大,于是把它暂停了,再由用户决定删不删这个进程)
D:深度/不可中断休眠
浅度休眠和深度休眠的区别是什么呢?
浅度休眠:我们可以CTRL+C来杀掉(中断)浅度休眠的程序
深度休眠:操作系统无权中断深度休眠的程序,常用于防止重要数据的丢失,也属于阻塞形态的一种
X:死亡状态对应的就是Linux中的结束状态(一般处于X状态操作系统就直接消除掉了,一般看不到)
Z:僵尸进程(为了获取程序退出的信息,一般是父进程获取子进程的信息,子进程提前退出,父进程还在运行导致子进程的退出信息一直没有被接收,此时子进程就会处于僵尸状态,代码页表数据被释放,而PCB不被释放,其会等待父进程运行完接收子进程的退出信息后结束)
2.4.僵尸进程及危害
如何理解僵尸进程:举个例子我们在创建子进程的目的,是为了让子进程完成某种事情的,而子进程相关的结果,父进程得知道吧,所以子进程在结束时其的PCB不会销毁,在父进程获取这个结果之前,这个子进程会一直处于僵尸状态
我们应该如何模拟Z状态呢?

此时子进程循环五次后结束,父进程一直循环打印但不接收子进程的结束状态,此时子进程处于僵尸状态(defunct=失效的)

若父进程一直不管,子进程的PCB一直存在,会一直占有内存,会引发内存泄漏问题!!!
知识点:1.进程退出了,内存泄露还在不在?
答案是不在的,进程退了,内存泄露会自动被操作系统回收,而常驻的进程出现内存泄漏才是最可怕的
2.在系统中会非常频繁的创建进程释放进程,所以操作系统会构建一个链表(unuse)来存放高频进程的结点(PCB),到时候用的时候直接拿再初始化即可

2.5.孤儿进程
子进程提前退出,会导致子进程进入僵尸进程状态,那如果父进程提前退出呢,此时子进程就被称之为孤儿进程
父进程提前退出,子进程的ppid(父进程id)变成1,此时子进程被1号进程(操作系统)领养,如果不领养会怎么样?

子进程如果不被领养,它的退出信息就没人接收,会处于僵尸状态,那么就没人回收这个子进程了,会导致内存泄露
1.那么父进程有没有孤儿状态呢?
父进程的父进程是bash,也就是说父进程随便退,bash进程会自动回收它
2.若这个子进程变成孤儿进程,它会变成后台进程,此时我们Ctrl+C就杀不死他了,它会一直打印消息,我们该如何消灭它呢?
答:我们可以再开一个xshell,再新的xshell中输入kill -9 (孤儿进程id)即可

3.进程优先级
3.1.基本概念
1.是什么?
优先级是进程得到CPU资源的先后顺序
2.为什么?
目标资源短缺,系统要通过优先级确认这份资源谁先谁后利用
优先级(能得到资源,先后顺序的问题)和权限(能否得到某种资源)
3.怎么办?
实则系统优先级就是一个数字,类型为int,存在task_struct中,它的值越低,优先级就越高;大多数操作系统是基于时间片的分时操作系统(每一个进程都有一个固定时间限制),要考虑到公平性,优先级可能变化,但是它变化幅度不能太大
3.2.查看系统进程
在linux或者unix系统中,⽤ps ‒l命令则会类似输出以下几个内容
UID : 代表执行者的⾝份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
NI :代表这个进程的nice值
我们可通过ps -al |head -1 && ps -al |grep myprocess来查看我们自己的进程优先级

UID:
我们可通过ls -ln来查询我们的UID,在操作系统中是通过UID来识别用户的,进程在启动时也会把这个UID保存起来来说明这个进程是谁启动的,所以当我们想访问某一个文件时候,操作系统会通过UID来进行对比,俩都等是拥有者,一个相等是所属组,都不相等是other,所以在Linux系统中,访问任何资源,都是进程访问,进程就代表用户

3.3.PRI和NI
PRI:进程的优先级,默认为80
NI:进程优先级的修正数据,为nice值
进程真实的优先级(PRI)=默认的PRI(80)+NI
3.4.调整优先级的方法(更改nice值)

![]()
nice是调整进程的优先级,renice是调整运行时进程的优先级
优先级的极值问题:
nice的调整范围是[-20,19]
故Linux的进程优先级范围是[60,99]
系统为什么给的优先级调整的范围这么低,不能在广些呢?
避免优先级设置不合理,会导致优先级低的进程长时间得不到CPU的资源,会导致内存饥饿
3.5.竞争,独立,并行,并发

并行:一个操作系统上可能会有两个CPU,那么有两个进程同时在这两个CPU上跑
并发:由于一个CPU只能运行一个进程,为保证多进程同时推进,CPU会给每个进程分配一段时间,使得多个进程来回切换,只要切换的速度足够快,用户就感觉不到
4.进程切换
4.1.引入
1.死循环如何运行
a.一旦一个进程占用CPU,会把自己的代码跑完吗?不会,操作系统会分配给他一段时间,没跑完要等下次调用再运行剩下的代码(时间片)
b.死循环进程不会一直占用CPU
2.CPU,寄存器
CPU内会存放很多寄存器,用于存放正在运行进程的代码(临时)

结论:
1.寄存器就是CPU内部的临时空间
2.寄存器(空间)不等于寄存器内保存的数据(内容)(左值和右值)
3.进程如何切换?

进程A的时间片到了后,会保存进程A的上下文数据(临时变量等),以便于下次还能正常使用,等进程B运行完后再将进程A的上下文数据恢复,然后开始运行后面的代码依次循环
a.所以进程A的上下文的数据保存到哪里了?
保存到TSS(任务进程段)中,可通过task_struct(PCB)中找到它
b.全新的进程Avs已经调度过的进程A
调度器=调度+切换
4.2.Linux中的调度队列

主要看蓝红框的数据,*active表示指在活跃队列的指针,*expired表示指在过期队列的指针,nr_active表示该队列中存放的进程数量,bitmap[5](int类型)为位图(共有5*32=160)用于快速查看各优先级是否为空(每一个比特位表示一个优先级),queue[140]为存放进程的队列,共有140个队列(哈希表)
queue[140]表示Linux的优先级,但我们Linux优先级不是只有40个吗(60-99),但实际上操作系统分为两大类型,一类是分时操作系统(时间片,常用于互联网领域),一类是实时操作系统(一个进程跑完再跑下一个进程,常用于应用领域),大多数操作系统两者都包含,只不过没有开实时操作系统,故0-99(实时优先级)不考虑,此时还剩下40个优先级(普通优先级)就是我们的分时操作系统的优先级,而queue的成员变量的类型为task_struct*,此时优先级相同的进程可以类似于队列一样存进去,先进先出(FIFO)

Linux中的调度队列有两个queue[140],分别为活跃队列和过期队列,一般进程刚开始都存放在活跃队列中,当这个进程的时间片在活跃队列到期后,若该进程没有跑完,它会放到过期队列中(此时处于就绪状态),此时活跃队列上的进程会越来越少,而过期队列上的进程会越来越多,直到活跃队列中nr_active为0时,此时swap(&active,&expired)实现队列切换操作,此时就完成了调度器的功能(调度+切换)
三.命令行参数和环境变量
1.命令行参数
在此之前我们先引入一个问题:main函数有参数吗?(我们在code.c文件中测一下)

答案是有的,那么argc和argv又都表示什么呢?
argc表示命令行参数的数量,argv(命令行参数表)表示存放的命令行参数(字符串),此时我们调用一下这个代码则会发现:

我们输入的数据以空格为分割符,存放在argv这个字符串数组中,那么他有什么用处呢?
此时我们将main函数改成这样,然后运行


对此我们可以推出,main函数的命令行参数,是实现程序不同子功能的方法(命令实现的原理ls -a -l)
我们知道要实现我们code功能,要先找到它,故我们要在code前加./,那为什么我们使用指令时不用在ls前加./呢?接下来我们通过环境变量PATH来解答这个问题
2.环境变量
2.1.环境变量PATH
系统中存在环境变量(PATH),来帮助系统找到目标二进制文件
PATH:系统搜索指令的默认搜索路径,我们可以通过echo $PATH来获取PATH的内容

一个冒号代表一个路径,系统内找命令时会在这些路径下一个一个找,若我们将我们的code粘贴到这个路径时(export PATH=$PATH:code程序所在路径),此时我们用code就不用指定路径了(不推荐,会污染指令池)
但我们退出重新登陆的话,code指令又用不了了,因为环境变量的生命周期只在我们登陆时有,退出后环境变量表自动释放了,若想一劳永逸,就要在bashrc文件(系统配置文件)中添加我们code所在的路径
我们的环境变量不止有PATH1个,可通过env来查看我们的环境变量,这些环境变量都存放在环境变量表中

当我们输入ls -a-l时,这个字符串会被我们bash通过命令行参数表先拿到,再从环境变量表中来找这个指令是否存在,所以bash中存在两个表,命令行参数表和环境变量表

那么环境变量最开始从哪来的呢?
我们先返回根目录(cd ~),然后再ls -al显示所有目录

此时环境变量都存在在bashrc文件中,我们在打开Linux时bash会自动根据系统相关配置文件(bashrc等文件)来生成环境变量表,若我们在这个文件中加入我们的code路径,我们下次打开就不用再手动在PATH加路径了
2.2.认识更多的环境变量
HOME:用户目录的家目录

USER:当前登录下的用户是谁

HISTSIZE:bash记录你使用过的指令(最近3000条)

HOSTNAME:当前登入的主机名

PWD:记录当前的工作路径

OLDPWD:记录上一次的工作路径(配合cd -(切换上一次路径))

2.3.获取环境变量
1.操作
a.export +变量名=内容(定义环境变量到环境变量表中)
![]()

b.env:显示所有的环境变量
c.echo $xxxx显示某一个环境变量
d.unset +变量名(删除某一环境变量)
2.代码
a.bash的环境变量表

在此之前我们要了解,main函数最多有3个参数,分别为命令行表参数个数,命令行参数表,环境变量表,在C语言中,我们会说main函数是程序的入口,实则不然,而是通过_start函数来调用main函数的,_start函数来判断main函数的参数个数来进行调用
故我们可以通过env(环境变量表)来获取环境变量,而这个环境变量表是父进程的(bash)给的,由此我们可以推出,在一个bash下(一个用户一个bash),环境变量可以由子进程继承,而子进程的子进程也可以继承bash的环境变量,故在一个bash下,环境变量具有全局属性
b.getenv(“变量名”)//获取环境变量的内容(使用前包含stdlib.h)

故我们可以通过getenv来实现只有我们自己能实现的程序
c.extern(声明) char **environ;(获取环境变量表)

上述ac是获取所有的环境变量,b是获取单独一个环境变量(推荐)
2.4.理解环境变量的特性
1.环境变量具有全局特性

我们在使用export时会不会有一个问题,我们都知道父进程的环境变量给子进程很合理,那我们子进程在使用export怎么还能给父进程添加环境变量呢?(子进程的环境变量为何会继承到父进程呢?),这里先粗略的说,export属于内建命令(命令执行时不需要创建子进程),这个命令会让bash自己亲自执行(bash自己调函数,或系统调用完成),这里就理解了我们之前删除PATH路径时为什么ls和pwd等命令还能执行(内键命令)
2.5.补充(本地变量)

set命令会显示环境变量和本地变量
本地变量,不会被子进程继承,只在bash内部使用(bash内部还有本地变量表)
有很多本地变量是具有特殊用途的

PS1是不同用户的命令行输出形式(root用户是$,普通用户则是#)等
四.程序地址空间
1.前提+引入
在C语言中我们学过

其中堆是向上存储的,栈是向下存储的,全局变量位于初始化数据和未初始化数据,正文代码只可读不可写
我们可以通过这段代码来观察各个属性变量的地址都在哪些


其中我们能够看出str(常量字符串)的地址靠近正文代码地址,实则就是存放在了正文代码段,故解释了常量字符串为什么不能更改,static修饰的变量test和全局变量g_unval地址接近,故static实则就是将普通变量变成了全局变量等等
此时导入我们之前写过的代码,还是定义一个全局变量gval,子进程更改gval值并打印,父进程只打印,我们上述可知由于写时拷贝子进程更改的值父进程还是保持原来不变,这次我们将父子进程的地址打印出来,看看会发生什么?


我们会发现父子进程的地址是相同的,这就奇怪了地址一样内存的数据为什么不一样?这就要讲到我们下面提到的虚拟地址
2.虚拟地址(1/4)
关于虚拟地址我们会讲到4次,接下来是第1次讲解,剩下的后面会提到
一个进程,一个虚拟地址空间,一套页表(用来做虚拟地址和实际地址的映射的),我们进程的PCB中会存在一个struct mm_struct* mm指针来指向这个虚拟地址空间

我们在写程序时,每一行程序每一个变量(所有数据)都有对应的虚拟地址,每个虚拟地址通过页表映射一个实时地址(物理内存),虚拟地址的宽度为1字节,在32位机器下长度为2^32(4GB),64位长度为2^64,由于地址宽度为1字节,而我们学过C语言都知道,一个整形变量大小是4个字节,每个字节都有自己的地址,共有4个地址,故这里我们取小的地址(起始地址)填入,然后我们用起始地址+偏移量获取数据内容
父进程创建的子进程也存在虚拟地址空间,也有一套页表,当然数据也全都是拷贝父进程的,里面的虚拟地址(0x11111)和实时地址(0x112233)都指向一个空间,但当我们在子进程进行w操作时,操作系统会将要修改数据的原先内容保存下来,并向下新开创一个空间(0x223344)并将数据拷贝到这个新地址空间中,操作系统会讲子进程的虚拟地址映射到这个新空间上(0x223344)
由于子进程和父进程的虚拟地址相同,只是映射的实时地址不同,故打印出来的地址是相同的

更准确地说,我们上述讲的是进程地址空间
2.1.如何理解虚拟地址空间
我们的操作系统管理多个进程时,它会分配给这些进程独立4GB的空间(画饼),这些进程他们互不相识,都会认为自己都会有独立充足的空间,实则他们根本拿不到4GB的空间(例)

画饼是什么操作?(就是一个虚拟地址表结构体里面存放着各个类型数据的地址和范围->struct mm_struct);这些“饼”也要管理起来,怎么管理呢?先描述再组织(通过数据结构管理起来)



2.2.区域划分
我们mm_struct作用是什么?是用来开辟空间,这些空间的值从哪来?是从程序加载时,初始化而来
什么是区域划分,只要确认区域的开始和结束即可
struct mm_struct中这些地址都是通过long类型保存下来的,故如果有个类型空间不够用了,他们会挤其他类型的空间,像更改区域是、的开始和结束(start,end)我们称为调整区域

调整区域划分:
1.在虚拟地址中申请指定大小的空间
2.加载程序,申请物理空间
3.1<->2,由页表实现映射,虚拟地址转化为物理地址(也可理解为物理地址转化为虚拟地址)
2.3.虚拟地址的作用
1.将地址由无序变有序:
在我们编写程序时,如果写了100字节的代码,对应的就会申请100个虚拟地址来存放这100字节的代码,由于我们前面知道操作系统在运行进程时不会一直运行他,是只会运行一部分此时我们的代码可能在这块实时地址有一部分,那又有一部分会非常无序,而我们虚拟地址就解决了这一问题,用连续的虚拟地址来映射无序的实时地址空间,这就是我们访问地址为什么会是连续的原因

2.地址转化的过程中,也可以对你的地址和操作进行合法化判定,进而保护物理内存:
在页表中,除了有我们虚拟地址,物理地址,还有权限;你要做w操作操作系统看你没有w权限,操作系统会把你这个进程直接杀掉,进而对物理内存实现保护
a.关于野指针:野指针也是地址,操作系统在对野指针通过页表转化转化不过去,进而直接杀掉程序(野指针不一定程序崩溃,因为他有可能会指向你的有效内存(数组越界))
b.修改常量字符串,char *str="abcde",str='H';这串代码可以编过,但在运行时会直接崩掉(字符常量区中没有w权限)
当我们编写程序所占内存比较大时(2G),操作系统会先将一部分(500MB)内存处理好(虚拟地址有对应的物理地址),后面只分配虚拟地址,没有对应的物理地址(画大饼),这时操作系统在识别这些没有分配的虚拟地址时,会将物理地址重新填配到页表中(500MB),依次循环,建设好映射关系,这个操作称为缺页中断(了解),在缺页中断时,进程是暂停的,该操作完毕后再继续调用该进程
3.让进程管理和内存管理进行一定程度的解耦合
左侧的虚拟地址部分是进程管理,右侧的物理内存和磁盘部分是内存管理,他们俩只通过页表来联系起来,耦合度低
澄清一些问题!
1. 我们可以不加载代码和数据,只有task_struct , mm_struct, 页表(缺页中断)
2. 创建进程,先有task_struct , mm_struct等,还是先加载代码和数据?
先有mm_struct,再有task_struct陆陆续续加载数据
3. 如何理解进程挂起???
内存区域严重不足时,操作系统会将页表中左边的虚拟地址保存(合法),右面的实时地址清空,把代码和数据唤出到磁盘中(挂起),下次再通过缺页中断加载进来,用多少加多少
2.4.虚拟内存管理
在此之前我们先引入一个问题:我们在堆区申请空间时,每申请的空间都不是连续的,故每个申请的空间的start和end都不一样,这说明他不止一个起始虚拟地址吧
此时我们在看mm_struct中仔细观察一番

发现mm_struct中存在一个vm_area_struct变量的结构体指针,vm_area_struct中存在start和end

mm_struct通过vm_area_struct来分配空间,在这些区域中每创建一个堆空间,vm_area_struct就根据这个堆大小分配start和end,再通过双链表链接起来,不止是堆区,其他区域也是如此

此图片堆区只有一个,若创建多个堆区也是通过vm_area_struct链接起来;我们可以看到各个区域是通过双链表链接起来的,vm_area_struct是对每一个区域空间进行描述,然后我们mm_struct是对整体空间的一个描述!!!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)