引言
本来在写一篇Windows内存管理的文章,写着写着就发现好多基础的概念都要先讲。更可怕的是,这些基础的概念我却不能完全讲清楚。只好再把这本《深入解析Windows操作系统》翻到第一章…… 很多书的第一章往往都会说:第一次读本章往往一知半解,建议学习后面的内容时隔段时间再回来看本章,会有更多的收获。这本书就是这样。
友情提示:结尾更精彩~
进程
尽管表面上看起来程序和进程非常类似,但本质上它们却是截然不同的。程序是一个静态的指令序列,而进程则是一个容器,其中包含了当执行一个程序的特定实例时所用到的各种资源。比如:Notepad是一个程序,运行这个程序便产生了进程。
从高层次的抽象来看,一个Windows进程是由以下元素构成的。
- 一个私有的虚拟地址空间。(虚拟地址空间指的是一组虚拟内存地址的范围)
- 一个可以执行的程序。它定义了初始的代码和数据,并且被映射到该进程的虚拟地址空间。即可执行映像。
- 一个已打开句柄的列表,这些句柄指向各种系统资源。该进程内的所有线程都可以访问。
- 一个被称为访问令牌的安全环境,它标识了与该进程关联的用户、安全组和特权。
- 一个被称为进程ID的唯一标识符。
- 至少一个执行线程。(第一个执行线程称为主线程)
进程、线程的定义很多,但像这本书这么直接了当的毫不抽象(貌似与上面某句话矛盾哈^_^)的定义倒很新颖。当然,说的这么细,人家就不一定能看懂了。
线程
线程是一个进程内部的实体,也是Windows执行此进程时的调度实体(抛开与进程的关系,线程是系统进行调度的单位)。如果没有线程,进程的程序不可能运行。
线程包括以下一些最基本的部件:
- 一组代表处理器状态的CPU寄存器中的内容的备份;
- 两个栈,一个用于当线程在内核模式下执行的时候,另一个用于在用户模式下执行时。可想而知,一个叫内核模式栈,一个叫用户模式栈。
- 一个被称为线程局部存储区(TLS)的私有存储区域。
- 一个被称为线程ID的唯一标识符(进程ID和线程ID在内部都叫客户ID,它们是在同一个名字空间中生成的,不可能重叠)。
- 有时也有线程安全环境。牵扯到Windows的模仿机制。
易失的寄存器、栈、和局部存储区合起来被称为线程的上下文(context)。
虚拟内存
虚拟内存提供了一个内存的逻辑视图,它并不对应于物理内存的布局。在运行的时候,内存管理器借助于硬件的支持,将虚拟地址映射成物理地址。
每个进程都有自己的虚拟地址空间,而且它会感觉到自己独占了这个很大的地址空间。在32位x86系统中,总的虚拟地址空间的大小为4GB。因为32位指针可以表示0X00000000到0XFFFFFFFF之间的值。但是默认情况下,Windows会将2GB的地址空间给进程,作为其私有地址空间,称为用户模式空间;而另一半(地址空间中较高的一半,从0X80000000到0XFFFFFFFF)则用做它自己的受保护的内核使用,称为内核模式空间。
虚拟内存有三个好处:
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的内存块。
- 程序可以访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。页面会根据需要在物理内存与磁盘之间换入换出。
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法访问正在由另一进程使用的物理内存。
内核模式和用户模式
CPU有不止一种特权级别,可以用来保护系统代码和数据不被低级别的代码修改。Windows就使用了CPU的两种模式,而将其称为用户模式和内核模式。用户程序代码运行在用户模式下,而操作系统代码运行在内核模式下。内核模式的权限更高:它允许访问所有的系统内存和执行所有的CPU指令。而只有操作系统的代码才能在内核模式执行,从而能防止恶意的应用程序破坏系统。
每个Windows进程都有它自己的私有地址空间,但内核模式的操作系统和设备驱动程序共享同一个虚拟地址空间。虚拟内存中的每个页面都标记了处理器在什么访问模式下才可以访问该页面。注意,设备驱动程序可不一定是微软写的,因此系统对恶意的驱动程序就缺少保护。这就是为什么Windows会更慎重的对待驱动程序,并引入了驱动程序签名。
应用程序在发出一个系统服务调用的时候,会从用户模式切换到内核模式,这时会发生什么?线程会切换吗?用户地址空间和内核地址空间不同,而页目录、页表又是进程相关的,怎么办?
从用户模式转换到内核模式,可以通过专门的处理器指令来完成。这条指令会将CPU切换到内核模式。操作系统会捕捉到这条指令,注意到有一个系统服务的请求到来,然后执行相应的内部函数。在将控制返回给用户线程以前,处理器的模式被切换回用户模式。
从用户模式到内核模式的装换本身并不会影响线程的调度——模式转换并不是环境切换。这就要说到系统机制了。
浅谈一点系统机制
当应用程序调用系统服务时,处理器将从用户模式切换到内核模式。这个过程是通过中断实现的。
中断
中断原本是硬件的概念,CPU提供中断的功能,它也外接中断控制器用来控制外围设备的中断请求。Windows系统在内核中定义了一套中断优先级方案,称为中断请求级别(IRQL)。下图就是x86系统的中断请求级别的定义。
优先级高的中断屏蔽优先级低的中断,即假如当前CPU正在处理一个优先级5的中断请求,它会将IRQL设为5,低于或等于5的后来中断都会被屏蔽。
普通线程运行在优先级0上(它实际上并不是一个中断优先级),因而会被任何中断打断。
准确地说,在x86早期的CPU上,采用int 0x2e这条中断指令来触发系统服务分发。发生中断后,CPU会去查中断分发表(IDT),根据中断号(0x2e)索引到中断分发例程,在这里就是系统服务分发器。
后来的处理器则提供了更高级的指令,而不是中断指令了。
重点来了!
回到上面的疑问,首先,由于系统调用只是处理器模式的切换,并没有发生环境(上下文)切换,所以线程和进程没有切换。
第二个问题,用户地址空间和内核地址空间不同,而页目录、页表又是进程相关的,进程又没变,这不是矛盾了吗?
答案是,进入到内核模式之后,内核模式的代码只能访问内核地址空间,不能访问用户地址空间。而由于内核地址空间对所有进程都一样,即所有进程都有相同的内核地址空间(只是在用户模式下访问不到而已),因此到了内核模式之后(运行在内核模式的)代码不会出错。
再详细的解释一下,页目录、页表与进程相关,指的是在用户模式下。即,在x86体系中,每个进程的虚拟地址空间的0x00000000~0x7FFFFFFF都是不相同的(映射到不同的物理内存地址),而0x80000000-0xFFFFFFFF都是相同的。所谓内核模式共享同一个虚拟内存空间就是这个意思。
但还有一个问题,既然运行在用户空间的代码访问不了内核空间,内核空间的代码也访问不了用户空间,那我进行一个系统调用的时候,参数和返回值怎么传递?
答案是,由内核(系统服务分发器)拷贝过去。
参考文献
《深入解析Windows操作系统》第四版
《Windows核心编程》
MSDN