一、总线
- 地址总线:地址总线AB是专门用来传送地址的,由于地址只能从CPU传向外部存储器或I/O端口,所以地址总线总是单向三态的,这与数据总线不同。地址总线的位数决定了CPU可直接寻址的内存空间大小,比如8位微机的地址总线为16位,则其可寻址空间为216=64KB,16位微型机的地址总线为20位,其可寻址空间为220=1MB。那为什么不能多次传输一个地址呢?这样不就能在有限的位数上寻址无线大的地址?真的可以这样吗,不能把软件上的逻辑带入到硬件上,硬件是非常简单的,他根本无法区分某一个地址是不是要和另外一个地址相加,所以只能把一次传输的地址当成最终目标地址。cpu将地址通过地址总线传输给内存,内存就直接到指定编号的地方读取数据,通过数据总线返回给cpu。
- 数据总线:cpu和存储介质交换数据的线路,决定CPU和存储介质互换信息的速度,宽度一般与地址总线一致,但不是必然。数据是可以多次传输的。
- 64位处理器、64位操作系统指的是什么:第一,这个位数是多方统一的结果,首先是cpu一次能处理64位的数据,其次也关系到了地址总线的宽度,数据总线的宽度等。第二、,编译器需要对64位的硬件进行优化编译,才能高效的发挥出64位硬件的效率。所以当我们在问64位到底代表什么时,会有各种回答,其实64位需要多个硬件共同配合的。
二、虚拟内存是什么?
虚拟内存是计算机操作系统提供的一种内存管理技术,它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。
注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。
现代所有用于一般应用的操作系统都对普通的应用程序使用虚拟内存技术,例如文字处理软件,电子制表软件,多媒体播放器等等。老一些的操作系统,如DOS和1980年代的Windows,或者那些1960年代的大型机,一般都没有虚拟内存的功能——但是Atlas,B5000和苹果公司的Lisa都是很值得注意的例外。
那些需要快速访问或者反应时间非常一致的嵌入式系统,和其他的具有特殊应用的计算机系统,可能会为了避免让运算结果的可预测性降低,而选择不使用虚拟内存。
虚拟内存多大合适?
swap分区的大小和实际使用的情况相关,如果你的机器硬件使用率比较充分,那么可以参考下面配置,如果你的机器硬件使用率平常就很少,交换分区可以设置更小,
4G以内的物理内存,SWAP 设置为内存的2倍。
4-8G的物理内存,SWAP 等于内存大小。
8-64G 的物理内存,SWAP 设置为8G。
64-256G物理内存,SWAP 设置为16G。
进程会占用多大内存?
一个进程在某一时刻占用的内存并不会太多,这个会由操作系统来管理,无需使用的空间会被换出到磁盘上,但是进程是可以申请很大的内存空间的,也是有能力将内存撑爆的。
三、内核空间和用户空间是什么?
操作系统的虚拟空间是被划分为用户空间和内核空间的,Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分, 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。有没有想过为什么每个进程空间都需要一个1G的内核空间,不是都共享一个的吗?那把内核空间单独拧出来,让每个进程空间全都是用户空间可以吗?一个虚拟空间应该是操作系统运行的一个完整空间,包含系统和用户程序,所有他们应该是一个整体,反正是虚拟的空间,没必要省,将每个进程空间都分配同一个内核空间并不会浪费,反而会使设计更加统一简单。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。
看一个进程切换的图
蓝色表示虚拟空间映射到物理空间,白色表示未映射,firefox占用了较多的内存,而ls程序使用了较少的内存。
进程虚拟空间的进一步细分:
几乎每个进程的虚拟地址空间中各段的分布都与上图完全一致。
这就给远程发掘程序漏洞的人打开了方便之门。一个发掘过程往往需要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间分布的一致性,来探索出这些地址。如果让他们猜个正着,那么有人就会被整了。因此,地址空间的随机排布方式便逐渐流行起来,Linux通过对栈、内存映射段、堆的起始地址加上随机的偏移量来打乱布局。但不幸的是,32位地址空间相当紧凑,这给随机化所留下的空间不大,削弱了这种技巧的效果。
进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储函数参数和局部变量。调用一个方法或函数会将一个新的栈帧(stack frame)压入到栈中,这个栈帧会在函数返回时被清理掉。由于栈中数据严格的遵守LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈中的内容,只需要一个简单的指针指向栈的顶端即可,因此压栈(pushing)和退栈(popping)过程非常迅速、准确。进程中的每一个线程都有属于自己的栈。
通过不断向栈中压入数据,超出其容量就会耗尽栈所对应的内存区域,这将触发一个页故障(page fault),而被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。如果栈的大小低于RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情。这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会栈溢出(stack overflow),程序收到一个段错误(segmentation fault)。
动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。
在栈的下方,是我们的内存映射段。内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux的mmap()系统调用或者Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,所以它被用来加载动态库。创建一个不对应于任何文件的匿名内存映射也是可能的,此方法用于存放程序的数据。在Linux中,如果你通过malloc()请求一大块内存,C运行库将会创建这样一个匿名映射而不是使用堆内存。“大块”意味着比MMAP_THRESHOLD还大,缺省128KB,可以通过mallocp()调整。
接下来的一块内存空间是堆。与栈一样,堆用于运行时内存分配;但不同的是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。在C语言中,堆分配的接口是malloc()函数。如果堆中有足够的空间来满足内存请求,它就可以被语言运行时库处理而不需要内核参与,否则,堆会被扩大,通过brk()系统调用来分配请求所需的内存块。堆管理是很复杂的,需要精细的算法来应付我们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变动。实时系统通过特殊目的分配器来解决这个问题。堆在分配过程中可能会变得零零碎碎,如下图所示:
最后,我们看看底部的内存段:BSS,数据段,代码段。
在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。区别在于BSS保存的是未被初始化的静态变量内容,他们的值不是直接在程序的源码中设定的。BSS内存区域是匿名的,它不映射到任何文件。如果你写static intcntActiveUsers,则cntActiveUsers的内容就会保存到BSS中去。而数据段则保存在源代码中已经初始化的静态变量的内容。数据段不是匿名的,它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。所以,如果你写static int cntActiveUsers=10,则cntActiveUsers的内容就保存在了数据段中,而且初始值是10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响被映射的文件。
你可以通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住:一个段可能包含许多区域。比如,每个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。有时人们提到“数据段”,指的是全部的数据段+BSS+堆。
你还可以通过nm和objdump命令来察看二进制镜像,打印其中的符号,它们的地址,段等信息。最后需要指出的是,前文描述的虚拟地址布局在linux中是一种“灵活布局”,而且作为默认方式已经有些年头了,它假设我们有值RLIMT_STACK。但是,当没有该值得限制时,Linux退回到“经典布局”,如下图所示:
C语言程序实例分析如下所示:
#include<stdio.h>
#include <malloc.h>
void print(char *,int);
int main()
{
char *s1 = "abcde";
char *s2 = "abcde";
char s3[] = "abcd";
long int *s4[100];
char *s5 = "abcde";
int a = 5;
int b =6;//a,b在栈上,&a>&b地址反向增长
printf("variables address in main function:n
s1=%ps2=%p s3=%p s4=%p s5=%p a=%p b=%pnn",
s1,s2,s3,s4,s5,&a,&b);
printf("variables address in processcall:n");
print("ddddddddd",5);//参数入栈从右至左进行,p先进栈,str后进 &p>&str
printf("nmain=%p print=%pn",main,print);
//打印代码段中主函数和子函数的地址,编译时先编译的地址低,后编译的地址高main<print
}
void print(char *str,intp)
{
char *s1 = "abcde";//abcde在常量区,s1在栈上
char *s2 = "abcde";//abcde在常量区,s2在栈上 s2-s1=6可能等于0,编译器优化了相同的常量,只在内存保存一份
//而&s1>&s2
char s3[] = "abcdeee";//abcdeee在常量区,s3在栈上,数组保存的内容为abcdeee的一份拷贝
long int *s4[100];
char *s5 = "abcde";
int a = 5;
int b =6;
int c;
int d;//a,b,c,d均在栈上,&a>&b>&c>&d地址反向增长
char *q=str;//
int m=p;//
char *r=(char *)malloc(1);
char *w=(char *)malloc(1);// r<w 堆正向增长
printf("s1=%p s2=%p s3=%p s4=%p s5=%p na=%p b=%pc=%p d=%pn
str=%pq=%p p=%p m=%p r=%p w=%pn",
s1,s2,s3,s4,s5,&a,&b,&c,&d,&str,q,&p,&m,r,w);
}
/* 栈和堆是在程序运行时候动态分配的,局部变量均在栈上分配。栈是反向增长的,地址递减;malloc等分配的内存空间在堆空间。堆是正向增长的,地址递增。
r,w变量在栈上(则&r>&w),r,w所指内容在堆中(即r<w)。*/