进程地址空间
操作系统在管理内存时,每个进程都有一个独立的进程地址空间,进程地址空间的地址为虚拟地址,对于32位操作系统,该虚拟地址空间为2^32=4GB。 进程在执行的时候,看到和使用的内存地址都是虚拟地址,而操作系统通过MMU部件将进程使用的虚拟地址转换为物理地址。
进程地址空间中分为各个不同的部分:
(1)由于系统内核中有些代码、数据是所有进程所公用的,所以所有进程的进程地址空间中有一个专门的区域存放公共的内核代码和数据,该区域内的内容相同,且该虚拟内存映射到同一个物理内存区域。
(2)进程在执行的时候,需要维护进程相关的数据结构,比如页表、task和mm结构、内核栈等,这些数据结构是进程独立的,各个进程之间可能不同。这些数据结构在进程虚拟地址空间中一个专门的区域中。
(3)进程在进行函数调用的时候,需要使用栈,于是进程地址空间中存在一个专门的虚拟内存区域维护用户栈。
(4)进程在进行动态内存分配的时候,需要使用堆,于是进程地址空间中存在一个专门的虚拟内存区域维护堆。
(5)进程中未初始化的数据在 .bss 段
(6)进程中初始化的数据在 .data 段
(7)进程代码在 .text 段
(8)进程执行的时候可能会调用共享库,在进程地址空间中有一个共享库的存储器映射区域,这个是进程独立的,因为每个进程可能调用不同的共享库。
linux系统中进程的地址空间分布如下图所示,其中在32位系统中0-3GB为用户空间,3-4GB为内核空间:
ELF文件中的内容布局
ELF(Executable Linkable Format)文件是linux下的可执行文件。ELF文件的结构如下所示:
其中ELF头中存放各个段的起始地址和长度以及其他的信息,各个段中存放不同属性内容。其中c程序中的变量在ELF文件中的存储区域如下图所示:
.bss段的大小可以通过ELF头中的信息得到,但是这只是一个“大小”数据,告诉程序中的非初始化的全局和静态变量会共占用多少内存,在文件中并不会有它的空间,只有当可执行文件装载运行时,才会被分配内存(并且位于data段内存块之后),并且初始化为0.
当可执行文件被装载入内存之后,各个段装载进的虚拟存储区域情况如下图所示:
内存分页
操作系统在管理内存时使用分页机制,分页机制相比于不分段不分页避免了频繁的内存调入调出,同时相对于只分段更加充分的利用了内存资源。一般内存页面大小为4KB,32位系统中进程通过虚拟内存地址访问数据或执行代码时候,32位虚拟地址中的0-11比特位为页面内的偏移量;31-12比特用于标志虚拟页面。
如果31-12比特共可以表示2^20个页面,因此如果使用一个页表,则会有2^20个页表项,每个页表项4B,共有4MB,因此内核空间中对每个进程都需要有4MB的空间存储页目录。而如果采用多级页表机制,31-22比特位为页目录表索引,页目录表中存放的是页表的地址,共需要2^10x4B = 4KB,每个页目录表项指向一个页表,每个页表的大小为 2^10x4 = 4KB,这样只需要在内存中存放 8KB的页目录表+一个页表就可以将虚拟地址转换到物理地址(
进程实际执行的时候,局部性原理使得大部分内存访问可能都在一个页表所能索引到的的4KBx1K = 4MB的内存区域中)
在实际进行虚拟地址到物理地址的转换时,需要使用MMU,MMU是一个硬件电路,包含两个部件,一个是分段部件,将一个逻辑地址转换为一个线性地址,一个是分页部件,将线性地址转换为物理地址。
程序启动
有了以上的进程地址空间分布和可执行文件的分布信息,就可以描述进程启动过程了。程序启动时,操作系统会新建一个进程来执行该程序,主要分为三个步骤:
(1)操作系统分配一个独立的进程地址空间,主要是在内存的内核区域中新建一个描述进程的结构体(linux中为task_struct),结构体中包含了进程的相关信息,比如进程运行状态,进程的寄存器,进程打开的资源,以及进程的内存管理结构(在linux中为mm_struct,进程的内存管理结构就描述了进程的虚拟地址空间的布局). 同时,为该进程创建一个页目录表。
(2)读取可执行文件头,建立可执行文件中各个段和进程虚拟地址空间中各个段之间的映射关系。当程序运行时需要将可执行文件中的内容载入内存来执行,比如在进程访问某全局变量时,该全局变量还没有被载入内存,此时需要知道该全局变量对应在可执行文件的什么位置。于是我们就需要知道进程中虚拟地址到可执行文件中位置的对应。
(3)将PC指针指向进程的代码入口处,开始执行
执行的时候会不断的发生缺页中断,发生缺页中断时会将实际的可执行文件中的内容载入到物理内存中,然后建立虚拟内存页和物理内存页的映射关系。
系统对进程的管理
操作系统内核区域中存储了各个进程的结构体信息,linux中为task_struct,task_struct中包含了进程的相关信息,比如进程状态,寄存器,内核栈,状态字,内存分配mm_struct。针对单独一个进程,它在运行的时候使用并更新task_struct中的信息,比如使用mm_struct用于访问内存...
在进程切换的时候,系统将原进程的相关信息保存到它对应的task_struct中;然后选择另一个进程,将task_struct中的信息装载到机器的寄存器中去,然后新的进程就按照它的task_struct来指导运行....
fork进程
在linux中,fork是一个系统调用,用于复制当前进程得到当前进程的子进程。fork时,系统在内核中新建一个进程结构体task_struct,由于开始时父子进程的大部分信息都相同,所以该结构体大部分信息都拷贝自原父进程的task_struct。
此时,子进程的虚拟内存页和父进程的虚拟内存页使用相同的物理页。当发生子进程或者父进程要对内存进行写操作时,系统再为子进程的对应的虚拟页分配物理页,即copy-on-write机制。