深入理解内存:原理简介

我们知道冯.诺伊曼体系结构中是把计算机划分为输入设备,输出设备,存储器,控制器,运算器.

输入设备主要是键盘,鼠标,输出设备主要是显示器,打印机

控制器与运算器我们一般全称为CPU.

存储器主要指硬盘与内存.

 

  为啥windows上的应用软件都差不多一样

我们开发软件时不会直接面对硬件(只有开发操作系统或驱动程序才直接面对硬件),操作系统封装了硬件的细节信息,应用软件只是使用操作系统提供的API去间接的操作硬件.以windows为例,我们发现运行在上面的软件不管是啥编程语言开发的,用起来貌似都差不多.界面上基本上都是那个些控件,比如些菜单栏,工具栏,下拉列表,单选,多选按钮之类的.几乎都一个样.原因很简单,实际上不管啥编程语言,你自己写的函数或者调用一些类库,如果要使用那些硬件资源,最后都归结于去调用windows的API.

所以操作系统是在硬件的基础上抽象一层,只提供给你一些API,然后各种编程语言又在API的基础上再封装一层,提供给你一些语言特性和类库,库函数给你用.

 

  数据处理和数据显示

看下windows的API(实际上可以简单的看成一些C语言库函数),我们发现绝大部分API函数都仅是实现处理数据和显示数据这两功能.

API中有很多针对鼠标和键盘的函数,我们可以把这看成是输入数据(信息).消息机制相关的函数就是处理数据. 进程和线程也可以看成在内存中处理数据

然后API中窗口,子窗口控件,菜单,GDI,文字和字体,位图那一堆堆的函数可以看成是显示数据.把数据通过一些好看点的图形界面显示出来.

以前很多人都认为程序 = 数据结构 + 算法,在那会没太注重用户体验,没太注重GUI时是很恰当.现在应该说程序= 数据处理(数据结构和算法) + 数据显示.

 

  磁盘和内存原理

我们在程序中的数据处理主要是操作磁盘和内存,硬盘是磁盘里面最常用的一种.以前有所谓的软盘,也是磁盘的一种,原理和硬盘是一样的.都是利用磁性物质的特性来保存信息.磁盘的原理就是利用电磁转换,学物理时我们知道电可以使物质带上磁性,而金属在磁场运动时切割磁感线时会产生电流.磁盘上有很多微粒的磁粉.当写通过磁头写数据时,磁头中的电流会导致磁粉极化,改变方向.读数据时,导体磁头经过磁粒的区域时会产生电流. 反正大概意思就是这样.

而内存的原理就完全不同,内存是晶体管制作的(CPU也是晶体管做的),而晶体管的特性就是我们平时常说的用开关的开和关来表示1,0.通过一些门电路的组合可用来表示数字和实现复杂的逻辑功能.而内存主要是用来临时保存数据.CPU就是处理一些逻辑关系.

晶体管由于必须得通电,然后用电流的有无状态来表示信息,所以只有通电的时候可以保存数据,电一断内存里的晶体管状态就处未知状态就啥用处也没了.而磁盘断电后磁性物质还会一直保持原样.

我们知道访问磁盘时就通过磁头去指到固定的地方然后读取数据.不过内存就不一样,不需要啥磁头去读取数据,它是有数据总线连接,我们是通过总线去读取内存的数据.

       实际上我们平时在程序中说要分配一块内存(比如new 或malloc),或者操作内存(读写数据),实际上没有真正的直接去操作内存,而只是告诉操作系统我要做这些操作,然后操作系统再给你去直接操作内存.同样我们操作硬盘时,实际上也是操作系统去读写硬盘.

       在windows中有所谓的虚拟内存技术,实际上就是由于我们不会直接操作内存才会有这种可能.我们当需要多少内存时,比如4G,而实际内存才2G,但操作系统却说可以有4G内存分配给你.它是把一部分硬盘也当作内存给你分配(至于把多少硬盘空间当作内存用这是可以手动配置大小的). 由于CPU读取数据时只会在内存中进行(不是必须非得这样,因为如果直接读取硬盘速度会很慢),所以当你使用了虚拟内存后数据会在内存和硬盘之间来回的交换.

等会再详细讲下虚拟内存...

简单的历史故事

1.单用户使用内存

刚开始的时候电脑功能相当简单,而且用电脑的也都是些高手专家,对硬件相当的了解.此时也没有啥操作系统,(实际上可能显示器也可以不需要),电脑磁盘上除了保存一些供你调用的标准库函数外啥都没有.开机后CPU仍然静静的呆着不干嘛,内存里也是空的啥东东都没有(现在我们用的电脑一开机,内存至少会加载操作系统代码,所以永远不可能是空的).

此时你要做啥操作就敲入些命令,然后就调用保存的库函数,自然是先把库函数加载到内存,然后再在CPU上面跑,跑完了可能就是打印些啥信息出来(此时还没文件的概念,磁盘上不会保存你的结果数据).然后接下来CPU就休息去了,内存也空了.你不去调用库函数时,电脑就傻傻的呆了啥都不干.

 

2.批处理

像上面说的你敲入些命令时电脑才给你干活,这样效率太低了.于是做了些改进,首先是出现了操作系统的雏形,打开电脑后会加载一些操作系统的代码常驻在内存中.此时出现了文件,可以把很多待操作的命令事先写好,然后保存成一个个的文件.然后电脑可以按顺序给你从头跑到尾,就就是你启动一个操作后就可以走人了,然后计算机会全部给你按顺序处理完,一次处理一大批嘛.所以叫批处理.

此时内存会被分为两块,一小块给常驻内存的操作系统.其他的给用户程序跑(内存和CPU只能给单个用户程序用)

 

3.保存,切换

批处理只能从头到尾按顺序跑,如果中间某个程序要跑太久太久,后面的就没法跑了.一个程序一当跑起来你如果把它停了下次只能重新再跑.于是有人琢磨着如果程序在内存中跑了一段时间还没完,可以把内存中所有的信息切换出来保存到磁盘上.等下次再跑时可以不用从头跑,而是再把磁盘上的信息切换进内存接着上次的跑.

 

4.分时,多进程

前面说的三种情况都是一次只能一个用户程序在跑,此时有多少内存就用多少,不够用就报错呗,很简单,没涉及啥太复杂的技术.但是这样显然不能充分利用CPU,内存资源.因为内存变得越来越大,而一个程序可能只用占一点点内存,这样会空出一大片.而且CPU也是跑得贼快的.一眨眼就跑完了,只有你进行那些IO操作时可能就慢的像蜗牛了.于是有人就琢磨着,应该可以让几个程序同时跑,一个程序对应一个进程,都同时在内存中占一片地盘.然后就通过分时轮流使用CPU.

此时操作系统做的事就多很多了啊.首先是要保证每个进程的独立,不能让一个进程轻易使用别的进程的内存了.然后有时还得实现进程间的通信,要防止死锁之类的麻烦事.

进程加载进内存,等待获得CPU,然后运行

内存分配

当出现了多进程的概念后.自然就需要想出很多花样出合理的分配和利用内存这个宝贵的东东.操作系统内存管理中涉及到的术语可能看得人头大.实际上用通俗点的话归纳下就做两方面的事

 

1.合理的给每个进程分配一块内存.当内存满了时把一部分进程的内存切换出去放硬盘上.恰当的时候再从硬盘切换回内存

2.虚拟内存技术.实际上就是把硬盘也当内存用,只不过要多做些切换罢了,在硬盘和内存之间切换.

 

内存地址

先不管内存怎么分块.我们首先会想到要用内存该怎么去访问? 你可能说肯定是通过地址嘛.硬盘访问就是通过一个地址,然后用磁头去读写嘛.

 

内存与硬盘间来回切换,会导致内存地址变化

如果只让一个进程在内存中,开始跑时全部加载,然后跑完了再释放内存.这样像硬盘一样直接通过内存的实际地址访问是没太多问题的. 但如果是多进程共用内存,并且还会在硬盘和内存间切换.此时就会有问题.假如你的程序跑了一部分,然后全部切换到硬盘,那些保存的信息里面有涉及到内存地址.等再切换到内存中时,你之前占的那内存可能被别的进程占用了,而系统分给你的是另外一块内存.那样的话你根据之前那些内存地址信息再读写数据时就会出错.咋整呢?

 

内存物理地址与逻辑地址

像在C,C++中我们一般用指针来指向内存地址.那你可能会问指针指的内存地址是内存的物理地址吗 ? 

如果是实际地址的话就出现上面说的问题了. 指针指向的实际上是一个逻辑地址.你可以这样想,在一个进程内我们先把所有指针指定一些固定的值(也就是逻辑地址),比如从0开始递增,然后实际上加载到内存中时(每次加载时的起始地址可能不一样),再用逻辑地址做偏移量加上内存中的实际起始地址就是实际的内存地址了. 你可能会奇怪那么每个进程的内存块的起始位置放哪里啊? 那个起始地址值是放在寄存器中.寄存器实际上可以看成一个速度更快的内存,在CPU呆一起,所以划分模块时我们一般分到CPU那一块去了.

当然从逻辑地址映射到物理地址肯定不止一种方法的,如果用到了分页技术,我们还可以把每个逻辑地址映射到物理地址中的不同页面(page)

不过上面说的也不是太准确,我们只能说我们用的绝大部分应用程序中涉及到的指针指向的内存地址是指向逻辑地址的.实际上也有些特殊的情况指针会指向内存物理地址,比如在嵌入式系统中,可能就一个进程用那内存,不用做啥切换,我们就可以用指针直接指向实际的物理内存地址.另外一般的桌面电脑中,内存中会划分出一块固定的内存来加载操作系统代码.操作系统中一部分内核程序会常驻内存,一直呆一个固定的地方,不会被切换到硬盘上去.此时指针也可以直接指向物理内存.

 

虚拟地址空间

 

虚拟地址与逻辑地址关系

毫无疑问除了物理地址是真实存在的内存地址外.其他地址都是我们虚构出来的.我们知道操作系统把硬件封装抽象了,我们编程时基本不会直接面对硬件.那我们可以把虚拟地址空间看成是抽象出来的内存.每个程序运行时(在windows中,linux或其他系统我还不确定)系统会给程序分配一个虚拟地址空间,就相当于给你一个内存,你尽量去用就是,不用管其他的事了.当你申请一块内存后会返回内存地址,这个就是逻辑地址,逻辑地址就是指向虚拟地址空间中某块地址

 

虚拟地址空间大小

在32位windows系统中,可用的虚拟地址空间是2的32次方,也就是4G. 你可能会奇怪这是怎么算出来的啊.因为32位系统顾名思义是因为处理器一次只能处理32位bit的信息,这样一来地址总线传的内存地址最多只能用32个bit来表示,所以只能表示4G的内存地址了.不过我们会其中的2G分给系统空间,只留下2G给用户空间.所以任何一个win32应用程序运行时系统会给分配2G的虚拟地址空间. 不过你可以指定让用户空间超过2G,最大可增至3G,不过这样系统空间就仅有1G了.

在64们的windows系统中,可用的虚拟地址空间是2的64次方,貌似是40多亿G.但显然我们用不了这么多,用了也没意义.所以我们只用到这些虚拟地址空间的一部分.8T用户空间, 248T系统空间.所以任何一个win64应用程序运行时系统会给分配8T的虚拟地址空间

我们知道当多进程在内存中共存时,如果内存足够大都够用,大家相安无事自然是最理想的事了.不过现实往往没那么美好.有些时候内存会满了,不够用了.此时必须将进程从内存移到硬盘中去.有空间时可能又会被移回内存来.专业点的说法叫 滚出(roll-out),滚进(roll-in).或者叫换出,换入.

 

在进行换出,换入时你可能会想到两种方法

(1)以进程为单位换出换进,这是最简单最容易想到的.但是显然不够灵活,因为一个进程所需的内存空间较大.所以这种交换技术现在用的不多

(2)只交换进程的一部分. 通过页式或段式内存管理先把进程的虚拟地址空间划分为若干页面或段,这样交换时就可就交换页或段.

 

页式存储管理

所谓分页就是把进程的虚拟地址空间划分成大小均匀的一页页的(实际上就是一块块的,把进程切成几小块罢了),比如一页是1K,然后把物理内存也划分成一页页的.然后再把两者映射起来.如下图.哎发现有时说一堆话学不如一个图顶用,下面的图都是我们从别处拷来的,图片原地址http://www.doc88.com/p-982340562158.html

                           

当然这个映射关系的信息肯定要保存在哪.都保存在寄存器中.然后通过逻辑地址找物理地址的流程如下图

                       


段式存储管理

分页时,就是不管三七二十一,都一刀切,把程序切成均匀的一页页的.但我们知道程序实际运转时是成为很多模块的,比如一个函数可能是一个模块.如果按程序的逻辑结构来分成更小的组成部分可能更合理.因为程序执行时也可以那样分成一个个小的单位去执行的嘛.这里我们把程序的更小单位叫作业

不过分段存储管理其实跟分页管理大的思想理念是一样的,都是把程序分成更小的单位嘛,便于交换而已.只不过分段不是均匀分成固定大小的页,而是根据实际情况分在大小不均的段.此时由于段大小不一,所以除了知道每个段的开始地址还必须要有段表长度的信息.

                 


分页与分段结合

分页与分段自然是各有好处.分段如果每段太大了自然不太好,那还不如干脆把整个程序交换出去得了,不用整得这么麻烦

于是有人想着把这两种方法结合起来,叫段页式存储管理.你要以在一段内再使用分页技术

虚拟内存

我们以前讲了在32的windows上,每个程序运行时都会分配2G的虚拟地址空间.就算你调大的话也最多就3G.从这句话里我们可以延伸出这样一些结论.

1.由于32的系统寻址空间只有4G,所以你整个大于4G的内存完全是浪费资源.系统只会用到其中的4G,多出来的根本不会去用.

2.你可能想着每个进程都是2G的虚拟地址空间,那一个进程加载进内存岂不是会把内存塞满了啊.实际上不会的,因为一来嘛分配给你2G地址空间.你不一定用这么多,可能只用10M,这样加载到内存的时候只加载你实际用的.另外就是不会一次把所有进程需要的内存分配下来然后把程序加载进来,而只是加载暂时需要的程序代码或数据

3.因为每个进程地址空间最大也只能整个3G出来.所以如果你的进程一跑时需要4G的地址空间才够用.那你的电脑肯定没法支持的.所以如果一些大型游戏需要内存特别多,你32的系统不管怎么整就玩不了.

 

另外你就肯定会有疑问. 过去个几年我们买电脑时貌似内存很多都512M,1G就算多了.然后嘛基本是32的系统.那如果某个程序实际有用到1G内存咋整? 因为虽然说程序中分配时是用的虚拟地址空间,但如果你在那2G的虚拟地址空间中实际有用到1G,最后就要映射到实际内存中去的啊.而且你1G的内存地址可是不能出现重复.这样那512内存肯定不够用的.

后面就出现了个虚拟内存的概念.就是划出一部分硬盘来当作内存用.当在虚拟内存空间中实际用到的内存大于物理内存时需要用到虚拟内存.在windows中可以在Advanced system settings那里面去设置虚拟内存的大小.不管如果是32位的话虚拟内存加上实际内存肯定也不能大于4G,不然多出来的那部分也是没有用处的.

假如还是上面的例子,512M内存,然后有1G的进程.你于是可以弄个1G的虚拟内存.然后映射的时候,先在实际的内存中映射,可能除开系统需要的一部分外就剩下300了.于是就先映射这300的内存,剩下的再去虚拟内存中映射.

反正你的用的时候只要发个逻辑地址过去.剩下的事就不用管,操作系统有个MMU(memory management unit),里面会有个页表.反正你输入逻辑地址它最终给你转换成物理内存地址或硬盘地址.

至于具体细节比较麻烦的.大概思路是,进程一般会采取分页技术,分成大小一样的很多页,每页有编号.然后页表里面会把你每页对应内存中具体的一块内存.当超过那300的实际上就对应于虚拟内存中去了.(实际上就是硬盘上的一个swp文件),肯定也有标志信息表明这部分是在硬盘上. 于是当进程运行起来时,需要用到某个地址时就映射到虚拟内存中时就会出现所谓的缺页,那页不在内存中嘛,于是就需要去硬盘上把数据读进内存来,如果内存满了就需要置换出去一些页. 所以你设置了虚拟内存,最后如果真的会有用到,那你会看到硬盘转的很快,然后程序运行速度会变慢.硬盘的操作比内存慢很多





展开阅读全文

没有更多推荐了,返回首页