程序载入内存后的结构

来源:互联网 发布:路由器数据监控软件 编辑:程序博客网 时间:2024/05/16 12:46

首先我们以Linux为例来研究,这代表了现在操作系统的有多进程的处理方式(后面我们会分析dos的处理方式)


来看一幅很经典的内存结构图:

------------------------------低地址

代码区                        |

------------------------------

全局数据区                 |

------------------------------

堆                               |

↓                                 |

                                   |

↑                                 |

栈                               |

------------------------------

系统空间                     |                    

------------------------------高地址


  这幅图你可能看过好多次了,但需要搞明白的是,这个结构不是针对整个内存而言的,而是针对一个进程而言的,每个进程都对应这样一个结构。


  在Linux中,从代码区到栈称为用户空间,大小为3G,剩下的系统空间大小为1G。你可能会疑问,这样的话一个进程不就占满了内存了?

  这是因为用到了页表机制。

  用户进程的内存空间是系统内核分配给该进程的VM(虚拟内存),并不表示这个进程占用了这么多的RAM(物理内存)。Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,用户看到和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。

  因为RAM中不存放所有程序运行的数据,所以可能会出现要用的内存页没有驻留在RAM中,即在RAM找不到与之对应的页框的情况,这时会发生缺页异常(对进程来说是透明的),内核便陷入缺页异常处理。

  发生缺页异常有几种情况:1.只分配了线性地址,并没有分配页框,常发生在第一次访问某内存页。2.已经分配了页框,但页框被回收,换出至磁盘(交换区)。


  好了,说了这么多,终于解释清楚了为什么进程会有这样的结构,关于这个结构要注意两点:

  第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。例外情况只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。

  第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。


  现在有了多进程,那么进程之间是如何切换的呢?

  我们从微观上CPU上执行的程序主体来说明,这个过程有三个主体:首先是被切换的进程,然后是内核中负责进程切换的程序,最后是被选中的切换后的程序。

  进程切换是有条件的:一是被切换的进程完成了程序代码的执行,通过一种机制调用内核进程切换处理程序做响应处理;二是由于进程等待资源而被放入等待队列,或执行睡眠调用而被放入睡眠队列;三是时间片执行结束,需要让出处理器;四是发生硬件中断。


  用户空间和系统空间最重要的就是用户栈和内核栈了,关于这两者需要注意:

  1、内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。记住,进程对应的用户栈和内核栈都是进程私有的。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。

  2、每个进程在创建的时候会在内核空间连续分配两个page即8K的数据用来保存进程结构(task_struct),这个进程结
构大概有1K左右,剩下的7K用作该进程的内核栈(写中断程序的时候不要用什么递归,大的局部变量)。 也就是说, 
除了每个进程都有一个用户栈之外,同时都有一个系统空间栈。实际上,进程的task_struct结构所占的内存是由内核动态分配的,更确切地说,内核根本不给task_struct分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分给task_struct使用。

  3、进入内核态与返回用户态对堆栈的使用是平衡的:在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。


  在进程切换时会发生进程用户态到核心态的转换,它是怎么发生的呢?

  当进程因为中断或者系统调用而陷入内核态时,进程所使用的堆栈也要从用户栈转到内核栈。进程陷入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆栈指针寄存器的地址为内核栈地址(CPU从任务状态段TSS中装入内核栈指针esp),这样就完成了用户栈向内核栈的转换; 当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
  关键在进程从用户态转到内核态的时候,进程的内核栈总是空的(理由见上面的3)。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。


  陷入核心态之后,内核是根据什么来获得进程的信息呢?

  这里就是PCB(进程控制块)的功劳了。 

  PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息,它使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位,一个能与其他进程并发执行的进程。

  内核中维护了一张PCB表,因此可以获得所有进程的信息。


  用户态、内核态之间的共享:
  1、我们知道linux的虚拟地址空间是内核态使用3G以上的高地址空间,那么所有的用户进程是如何共享这一个内核空间的呢?
Linux系统中的init进程(pid=1)是除了idle进程(pid=0,也就是init_task)之外另一个比较特殊的进程,它是Linux内核开始建立起进程概念时第一个通过kernel_thread产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的/sbin/init程序,期间Linux内核也经历了从内核态到用户态的特权级转变,/sbin/init极有可能产生出了shell,然后所有的用户进程都有该进程派生出来。而linux采用2级页表(1K x 1K x 4K),页目录的1/4(3G/4G)即256B是属于内核的;所以创建用户进程时会复制init进程的这256B的页目录以及后面的一级、二级页表,也即实现了内核空间的共享。

  2、一个进程在内核态 可以直接通过虚拟地址访问其他进程内核态的数据,因为他们是一个页表;一个进程在内核态 不可以直接通过虚拟地址访问其他进程的用户态的数据,因为他们不使用同一个页表。 



下面我们介绍dos下的机制:

  dos下没有多进程,系统与用户程序交互是根据PSP(程序段前缀)中的信息完成的。由于dos下是实地址模式,cs、es、ds等段寄存器是可以由程序设计者更改的,因此,程序载入内存后的结构没有固定的如同保护模式下的结构(最开始的图)。

  以下信息节选自《Intel汇编语言程序设计》:









  







原创粉丝点击