内存空间分布:在linux中,每个进程都有自己独立的4G内存空间。其中内核会用3G以上的1G虚拟内存地址,0-3G的虚拟内存空间可以被一个进程的用户态或内核态访问,但3-4G只能被进程的内核态访问。这里讲的空间是可以寻址的空间,不是说每一个进程都可以用满4G空间。3-4G中,896M是直接映射到物理地址的,128M按需映射896M以上的所谓高位内存。内核用的896M虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G)就等于物理地址。各进程用的是同一个内核(内核区对于所有进程是共享的;系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上(系统内核只有一个))。同样,这里指的还是寻址,实际使用前还是要分配内存。而且896M只是个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。

为什么地址不冲突:一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了4G。而用到的内存区域,会通过页表映射到物理内存。所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。每个进程进程一个页表,页表的起始地址放在进程的pcb中,当某进程运行时,将其页表的起始地址放在页表寄存器中。单CPU系统中只能有一个进程处于执行状态,因此一个页表寄存器可供系统中所有的进程交替使用

与实际结果也十分类似:

每个进程可寻址的空间多达32位,48位,但进程不会用到其中所有地址的,像上图一样,他只会用到少量地址,用到的地址才会映射到物理内存中,映射关系才会被保存到页表寄存器所指向的页表中。

虚拟地址空间具体分布

配合上个图的上个图理解:

  • 虚拟地址空间中用户区地址范围是 0~3G,里边分为多个区块:
  • 保留区: 位于虚拟地址空间的最底部,未赋予物理地址。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址。(好像在linux系统是 0x0400800以下,在windows系统是0x040000以下)
  • .text段: 代码段也称正文段或文本段,通常用于存放程序的执行代码 (即 CPU 执行的机器指令),代码段一般情况下是只读的,这是对执行代码的一种保护机制。
  • .data段: 数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态变量。数据段属于静态内存分配 (静态存储区),可读可写。
  • .bss段: 未初始化以及初始为 0 的全局变量和静态变量,操作系统会将这些未初始化的变量初始化为 0
  • 堆(heap):用于存放进程运行时动态分配的内存。
    堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
    堆向高地址扩展 (即 “向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
  • 内存映射区(mmap):作为内存映射区加载磁盘文件,或者加载程序运作过程中需要调用的动态库。
  • 栈(stack): 存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址 “向下生长”,分配的内存是连续的。
  • 命令行参数:存储进程执行的时候传递给 main() 函数的参数,argc,argv [],env[]
  • 环境变量: 存储和进行相关的环境变量,比如:工作路径,进程所有者等信息

多级页表(个人理解),n 级页表的话,逻辑地址应该分为 n+1 个部分,左边的 n 个页号,右边的一个页内偏移,用页号去查,找到下一级页表在内存中的位置,在用第二个页号查询,获得下一级页表在内存位置,不断查询,直到找到最终的访问内存块号,加上页内偏移量就找到了物理地址。

汇编:

在阅读linux 源码时,必须掌握汇编,内核代码用的编译器是gcc,而gcc用的是AT&T的汇编格式,与MS 的intel有些区别:

1
2
3
4
5
6
7
8
AT&T: -4(%ebp)                         //相当于 Intel: [ebp - 4]
AT&T: foo(,%eax,4) //相当于 Intel: [foo + eax*4]
AT&T: foo(,1) //相当于 Intel:[foo]
AT&T: %gs:foo //相当于 Intel:gs:foo
AT&T: movl -4(%ebp), %eax //相当于 Intel: mov eax, [ebp - 4]
AT&T: movl array(, %eax, 4), %eax //相当于 Intel: mov eax, [eax*4 + array]
AT&T: movw array(%ebx, %eax, 4), %cx //相当于 Intel: mov cx, [ebx + 4*eax + array]
AT&T: movb $4, %fs:(%eax) //相当于 Intel: mov fs:eax, 4

为什么装入时重定位不能解决动态模块绝对地址引用的问题(cxy 233页,目前理解,有待进一步验证)

  1. 多个进程会共享一个共享目标文件的代码部分。代码部分通过绝对地址访问数据部分,但是每个进程都有数据部分的副本,所以不能把代码部分的引用写死了,对每个进程来说,引用地址应该不一样。(说的不对,是每个进程的主程序 访问 共享目标文件的代码 不一定是一样的)
  2. 转入后地址部分是code类型,可读可执行但不可写(似乎不是主要问题)。

解决方法:PIC

地址无关代码(PIC)是指运行和放置地址无关的代码。 其实这里PIC是一个相对意思,因为生成代码或多或少都使用了位置无关代码,该实现的基本思想是:把指令中需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分则在每个进程拥有一个副本。地址无关代码能够在不做修改的情况下被复制到内存中的任意位置。这一点不同于重定位代码,因为重定位代码需要经过链接器或加载器的特殊处理才能确定合适的运行时内存地址。 地址无关代码需要在源代码级别遵循一套特定的语义,并且需要编译器的支持。那些引用了绝对内存地址的指令(比如绝对跳转指令)必须被替换为PC相对寻址指令。这些间接处理过程可能导致PIC的运行效率下降,但是大多数处理器对PIC都有很好的支持,使得这效率上的这一点点下降基本可以忽略。

参考文章:

(12条消息) linux的虚拟内存是4G,而每个进程都有自己独立的4G内存空间,怎么理解?进程虚拟地址4G指拥有4G的寻址能力,需要页表转换为实际物理地址,每个进程用到的内核是直接映射,地址的进程地址-3G的关系-CSDN博客

(12条消息) 一文讲解,Linux内核——内存管理(建议收藏)_linux内核内存管理_Linux加油站的博客-CSDN博客

页表存放何处,多个页表如何共用页表寄存器 - enbug - 博客园 (cnblogs.com)

Linux内核虚拟地址空间 - 简书 (jianshu.com)

查看关于链接的更多内容请参考:

【图片+代码】:Linux 动态链接过程中的【重定位】底层原理 (qq.com)

Linux内核虚拟地址空间 - 简书 (jianshu.com)