欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪
文章目录
- 关于页表
- 程序权限
- 加载状态
- 可执行程序分段
关于页表
在前一篇博客中博主提到,页表是链接虚拟地址(mm_struct)和物理地址(存储器)的中间商,其主要作用是映射虚拟地址与物理地址。但是除了这两属性以外还有其他的属性。博主给大家扩展几个。
程序权限
进程的空间被分为栈区,堆区,静态区与代码区,这些区域都有对应的虚拟地址存储在页表当中,我们的子进程与父进程会共用代码区中的数据,如下图所示。
其实页表除了记录虚拟地址和物理地址之外,还有一个属性也要记录,那就是区域对应的数据的访问权限,以代码区为例,它们在存储器当中肯定是只可读取,不可写入的,我们拿linux的文件权限(rwx)来表示这些数据的权限。
像变量这些,其权限则是rw,即可读可写,要注意,无论变量是否加了const修饰,它们的权限也是rw的,比如这里有个const修饰的变量i,它的权限也是rw,const限定词只会影响编译器的检查(比如改了const修饰的变量会报错),而不会影响到改变量的属性。
要注意,字面常量不在栈区和堆区当中,而是在一个称为常量区的地方,这个区域的权限是r(可读不可写)。我们以一个经典的代码为例:
char *str="hello world";
str[0]='H';//error,虽然编译器不报错,但是运行程序会崩溃
所谓的字面常量,就是比如1,2,3和"hello world"这种写在赋值符号(=)右侧的常量,在也就是我们常说的右值(rvalue)。这种数据通常都是放在常量区的,因此它们在页表中的权限为r.
由于,在语法上,我们并没有给*str加上const修饰,因此编译器检查语法时,这个语法1时通过的,于是编译器不报错。但是在程序运行的过程中,由于要找到str的物理地址值,因此操作系统会在页表中查看str的权限,然后发现程序在执行的过程中,对一个权限为r(可读不可写)的对象进行了写入的操作,因此操作系统觉得这个进程是非法的,直接就将进程给杀死了。所以就导致了这个代码,在编译器检查方面是通过的,但是运行起来则会崩溃。
加载状态
我们想象这么一个场景,现在有一个1GB的进程要运行,由于cpu需要切换调度进程,因此在一个时间片后,这个进程就调度结束了,那么博主这里提出一个问题:一个进程运行时,操作系统会将进程从磁盘中读取到内存,那么操作系统是不是将整个进程都读取到内存当中呢?
当然不是,因为cpu中调度一个进程的时间片很短,因此在很多时候,一个时间片内进程都不会运行结束,而是可能执行了几万行代码之后就切换到下一个进程了。而那些执行过或者还没执行到内容,有可能是不会存在到内存当中的。因为cpu并不是只执行一个进程,而是多个进程,如果一个2gb的进程全都加载到内存当中,可能还有其他内存更大的进程在运行,那么其他进程怎么办。
在页表当中还有一个属性,我们将其称为内容加载状态,如果存在则设为1,不存在则设为0。比如一些执行过的代码,还没有使用的数据,都是只有虚拟地址,而没有物理地址的,因为它们还不存在,只有当它们存在了,才会有它们相应的物理地址。
这样做的目的其实就是节省空间,我们以现实为例,黑神话悟空的大小有足足100gb,而我们常见的家用的电脑则只有16gb或者32gb的运存(实际就是内存大小),如果黑神话悟空是直接将所有的数据都加载到内存当中,那么16gb的内存早爆了,但是实际上我们却能正常的运行它。比如我们切换场景的时候会加载数据,或者看一下过场动画,实际上就是给游戏准备它加载到内存的空间,如果加载完成,就能继续游戏了。
可执行程序分段
现在我们都已经了解了,一个可执行程序存在栈区,堆区等等的区域分段,来保存数据。还有一个页表来映射各个段中的数据状态,以及虚拟地址和物理地址的映射关系。
但是我们来思考一个问题,博主在前一篇文章中提到过,这些可执行程序分段(其实就是进程空间分段)是由一个叫做mm_struct的结构体来管理的。写过代码都清楚,一个结构体对象其实是要初始化的,但是我们的进程,每个进程的的变量个数都不一样(栈区空间不一样),堆区的大小也不一样,甚至连代码正文的大小都不一样,那么mm_struct是怎么初始化的呢?它又是靠什么数据来初始化的呢?这些数据又是从哪获取的?
这些东西我们也不知道,mm_struct也不知道,但是有一个人肯定知道,我们先不说,这里我们输入指令
readelf -S [可执行文件名]
这里博主随便写了一个可执行程序,命名为hello,代码也就不给大家看了。接着我们输入指令
readelf -S hello
这些数据其实就是一个进程的各个分段,比如栈区,堆区,代码区的地址啊,权限啊之类的东西,这说明一个进程要形成什么样的分段,并不是在该进程被运行时才产生的,而是它本来就有,但我们执行该进程时,task_struct就管理该进程,并且让mm_struct读取这些数据,在根据这些数据,创建出分段,比如代码区的地址,大小,权限等。
这说明,一个进程的分段并不是运行时才生成的,那么它是什么时候有的呢?没错,那就是编译期间生成的。编译器除了要编译我们写的代码之外,还有生成一些额外的信息,上述的分段就是由编译器生成的,然后操作系统在运行该进程时又将这些信息读取进去。这里说明一个现象,那就是编译器和操作系统其实是有关联的
!!!因此我们在linux上生成的可执行程序,在windows系统下却不能运行,反之亦然。