标签:用户数 表示 记录 变量 manage mamicode 无法 返回值 进程启动
Linux的虚拟地址空间采用“分段+分页”结合的方式实现。先看分段,之后再介绍分页。
分段是将内存划分成各个段落(Segment),每个段落的长度可以不同,且虚拟地址空间中未使用的空间不会映射到物理内存中,所以操作系统不会为这段空间分配物理内存。这样的话,内核为刚创建的进程分配的物理内存可以很小,随着进程运行不断使用内存,内核再为进程按需分配物理内存。也就是说,尽管地址空间的范围和物理内存大小一样,但不会将全部空间映射到物理内存。
对于Linux进程的虚拟地址空间来说,它的内存布局如下图。
虚拟空间分了如下几个段:
从上面的描述大概也能推测出,除了堆内存外,其它段落空间都是自动填充分配的,用户无法控制这些内存的使用。而堆内存段是用户能使用的自由内存区,绝大多数程序的用户数据都丢在这里面,算是一个大杂烩空间。
例如,下图中是一段C代码和内存布局之间的对应关系。
提示:其它语言的内存布局
上面的布局C程序的内存布局,也是Linux下进程的内存布局。其它语言(比如CPython)编写的程序运行起来后,只要是在Linux下运行,其进程的布局也会如此。只不过这些语言的程序中,全局变量、局部变量等可能和C的布局不一样,这和各语言的底层设计有关。比如C编写的某动态语言,它不要求指定变量的数据类型,那么在加载到内存的时候自然不知道该变量类型所需的空间大小,当它转换成C后(尽管不会真的转换成C代码),这个变量只能丢进堆内存作为动态数据。
使用分段的好处就是“各段自扫门前雪”,虽然在地址空间中每个分段的地址都是连续的,但实际上,每个分段映射到物理内存地址时是独立的,段与段之间可以不连续。这是因为CPU为每个段都使用一对(即两个)特殊的寄存器:基址寄存器和界限寄存器。
而界限寄存器中的值用来表示该段在物理内存中的大小,即已为该段分配了多少内存。当准备用虚拟地址加基址计算物理地址时,需要先根据界限寄存器中的值检查将要访问的物理内存地址是否超出了这个段的范围。如果超出了,则表示访问了不属于该段的内存,也即内存的越界访问,而用户进程是没有权限访问其它进程或未分配内存的地址的,这时会收到一个SIGSEGV(segmentation violation)信号并提示:Segmentation Fault,即段错误或段异常。收到这个信号后默认情况下会终止该进程,因为它访问了非法地址,但是可以设置该信号的信号处理程序,从而做出其它处理。
例如,下图中的进程访问了Kernel段或者unallocated memory部分的内存,都会报错。Kernel段除了内核进程,任何用户进程都无法访问,典型的地址是用户进程想要访问0x0地址时,而该地址属于Kernel,所以报错。而unallocated memory是还未分配的内存,界限寄存器会保护该段无法访问。
再例如,C数组的越界访问时也会出现该问题。下图直观地显示了在Windows中同样的内存越界错误。
也就是说,基址寄存器是用来转换地址的,界限寄存器是用来保护进程不越界访问内存的。CPU借助基址寄存器和界限寄存器管理并提供地址翻译和内存保护的功能,通常称为内存管理单元(Memory Management Unit,MMU)。
最后再说明一点,内存地址翻译的任务既可以由操作系统来做,也可以由硬件CPU来做。但如果完全由操作系统来完成,就需要频繁地陷入到内核态,这样效率会非常低。所以,这项任务交给CPU硬件来完成,操作系统只需在必要的时候介入,比如分配内存、回收内存等。
标签:用户数 表示 记录 变量 manage mamicode 无法 返回值 进程启动
原文地址:https://www.cnblogs.com/f-ck-need-u/p/11675346.html