进程编译过程

一个c程序,编译时分为三步:预处理,编译,汇编,链接

  • 预处理,展开头文件、宏替换,去掉注释,条件编译等 gcc -E hello.c -o hello.i
  • 编译,使用编译器将预处理文件hello.i编译成汇编文件hello.s gcc -S hello.i -o hello.s
  • 汇编,使用汇编器将hello.s编译成目标文件hello.o,二进制文件 gcc -c hello.s -o hello.o
  • 链接,使用链接器将目标文件与其它目标文件,库文件等连接为可执行文件 gcc hello.o -o hello.out

c程序在虚拟地址空间布局

56ac39f11d9b5e06f652289f70b24c0a

  • 代码段,cpu执行机器指令部分,可共享,只读。
  • 数据段,初始化后的数据段,如int a = 1;
  • bss段,未初始化数据段,如int a[1];
  • 栈,临时变量,每次函数调用时保存的临时变量,函数调用返回地址及调用者环境信息。递归函数每次调用自己会使用新的栈帧,不会影响另一次调用的变量。
  • 堆,动态分配的变量。

    内存地址空间概念

  • 逻辑地址,程序产生的段内偏移地址部分,程序员所能见到的。

  • 虚拟地址,由段选择符和段内偏移地址两部分组成的地址,也有书指出这就是逻辑地址。 a50b7f8cc4ad3d3bf827f14ecf0db346

1.表示最大虚拟地址空间:2^13*2*4G=64T,段选择符的13位索引,区分GDT和LDT的1个比特,32位逻辑地址 2.linux0.11内核中,给每个进程划分了64MB的虚拟内存空间。如果是现在Linux内核,32位系统则分配4G的虚拟内存。

  • 线性地址,由段选择符索引的段描述符所映射的段基址和段内偏移组成。如果未开启分页机制,则线性地址(80386是4G)就是物理地址。
  • 物理地址,地址总线能够寻址的物理内存地址

    分段机制

  • 多进程保护,防止一个任务访问另一个任务或操作系统的内存区域。通过给与每个进程独立的段表,每个进程就会有不同的地址变换。

  • 不同段之间的保护,如代码段是只读,其他段不能修改 f8544878cd76ea0c63d72f2a3903f962

  • 把虚拟地址空间中的虚拟内存组织成一些长度可变的段。如数据段,代码段等。

  • 把逻辑地址转换成线性地址。

    段描述符表

    段描述符的一个数组。包括GDT(全局描述符表)和LDT(局部描述符表) 5bb1c1494d6b836e5b699e44be8f1981

  • GDT 系统中所有进程共享的系统代码段或数据段由GDT映射,GDT的基址地址和长度保存在GDTR寄存器中。并且包含所有LDT的段描述符。

  • LDT 各个进程自己的段表。LDT的基址,段限长及段选择符存放在LDTR寄存器 695e34b1aa48276e0367abd593f745aa 一个进程就是一个任务,任务A和B除了能访问自身的代码段和数据段,还能访问共享的os代码段,os数据段。通过虚拟地址空间隔离每个任务。 发生任务切换时,LDTR更换成新任务的LDT,但GDT不变。

    段选择符

df633bd8d51b16ea5ce79adcd2dbd019

或称为段选择子,是选择段的一个16位标识符。

  • 描述符索引 指向段描述符表中的索引,用于找出段描述符。
  • TI 0表示在GDT中查找,1表示在LDT中查找。
  • 通常由加载程序进行设置和修改,不是应用程序。
  • 访问某个段时,必须已经把段选择符加载到一个段寄存器中。处理器最多提供6个段选择符的寄存器,CS(代码段),DS(数据段),SS(堆栈段),还有ES,FS,GS3个辅助的数据段寄存器。

    段描述符

    长度是8字节,是GDT和LDT表中用于描述段的数据结构项,包括段基址,段限长,段属性。由编译器,连接器,加载器或者OS来创建。 db90d99343b5391d0cb1484989705568 4G线性地址空间中段基址由3个分离的基地址字段组合形成。

    分页机制

  • 分段机制把逻辑地址转换成线性地址,分页机制把线性地址转换成物理地址。CR0寄存器的PG位决定是否开启分页机制。

  • 80X86使用4K(2^12)字节固定大小的内存页,每个页面4KB,对齐与4K地址边界处。4G线性地址空间划分成了2^20(1M)个页面。

  • 由于是线性地址空间4K大小的页面作为一个单元映射,并且对齐于4K边界,因此线性地址的低12位可作为页内便宜量直接映射,线性地址的高20位转换到对应的物理地址的高20位。

  • 提高内存空间利用率,如果用单级表来映射4G内存,则每个进程需要4M,用分页机制可以大大减少页表占用空间。 1.如果一个进程12M,则只需要3个页目录项表示,1个页目录表,3个页表,只需要16KB的空间。因为有很多逻辑空间不需要用到。 2.如果用1级页表映射,则需要4M。

    页表结构

    ed77f6a0d613d115fb826c185f823d79 页表中每个页表项大小为32位,4字节,每个页4KB,则需要2^20(1M)个表项,页表占用4M。

  • 一个进程的虚拟地址需要先通过LDT中的段描述符变换为CPU整个线性地址空间中的地址,再使用分页机制。

  • Linux0.11每个进程最大虚拟地址空间为64MB,因此每个进程的逻辑地址通过(任务号)*64MB,即可转换为线性空间中的地址 210c8706acbde00889fd55c3d439264d

页目录表

  • 一级表。具有2^10(1K)个4字节长度的页目录项,页目录项指向对应的二级表。页目录表占用4KB,一页大小。

  • Linux0.1x系统中内核和所有进程任务都共用同一个页目录表。为了让他们互不重叠,必须从虚拟地址空间映射到线性地址空间的不同位置,占用不同的线性地址空间。

页表

  • 二级表。长度也是1个内存页,每个页表含有1K个页表项,每个占4字节。每个表项含有20位的物理基址。
  • 二级页表分散在内存各个页面中,不需要保存在连续的4MB内存块中。且可以在需要时再分配。
  • 每个进程有自己的页表,如果每个进程能映射4G内存地址空间,则需要4M存放页表。10个进程则40M。

页表项格式

0ada2933a97cb8240d1bc7b2b5d32cda

  • P 位表示表项对地址转换是否有效。如果不存在,处理器会通过缺页中断通知OS把缺少的页表从磁盘加载进物理内存。 这就是涉及到内存管理的换入换出,页面置换算法LRU,当物理内存不够时,可以把最近最少使用的内存页置换到磁盘上的swap分区,当需要时再置换回来。

    总结

    段页结合的方式管理内存,能够保证程序分成多段,符合人的习惯,每段分成多个页,能高效的利用内存,减少内存碎片。 Linux采用的是内存管理方式是段页结合的管理方式,总览图如下: 0ecf1fcd9d527cecc5e933962ee30613