Win95以前系统基于Dos,只是在外面加了一层图形界面,不是基于事件响应的,线程也只是分时轮询。WinNT(new Technology)有了质的改变,才成为现代OS。现代OS有Linux和WinNT,一大特征是内核运行区和用户运行区,用户运行区之间彼此独立——线性内存运行空间和CPU运行状态支持独立。满足这一条才可以真正称为多任务OS,在这基础上才有现代OS特有的进程、IO访问等等功能。
PS:这一特征要求CPU硬件也支持状态保护,有状态寄存器存储CPU环境,硬件设定用户进程永远无法直接访问高位地址(内核区)。对应于现代OS,支持这点的386系列CPU也是质变的一代。
OS结构
Windows内核比Linux要大,是因为它把图形界面和视窗机制等功能也包含在内核中了,它把类似于Linux1.0的内核称为“核心层”,外围包了几层,直到提供给用户进程的接口层。
广义上说,Windows的System32 DLL都算操作系统的一部分,这部分供用户使用,总是运行在用户区。这是Windows OS提供给用户的真正OS接口——Win32API。当然,它也提供了Linux标准的C Runtime Class软件接口界面。但在此之下的实现都对用户透明。
OS管理层,是各个管理子系统,纵向的,具体的操作可能直接到HAL层和硬件打交道。
核心层,比较接近于硬件层,类似于Linux1.0的功能,初始化加载核心,设备驱动底层中断,异常处理等功能。核心层常驻内存,参与CPU时间片轮询,但不允许其它线程抢占,只有执行完一个CPU时间正常退出。(早期OS不支持多任务,一个程序执行就会从头执行到尾,需要程序员分配PeekMessage;NT系列OS支持多用户空间,CPU抢占方式控制用户任务轮流执行。)
系统调用
进程 ——> 调用OS API;OS进程管理 ——> 调配进程。
仅从用户进程角度,OS就像是一个被动响应的运行时库。Windows提供了一个系统调用界面作为外层,即Win32API;Linux的CRuntime库标准,Windows也支持。用户进程通过调用这些OS接口,可以部分操作系统内核。
硬件] 设备驱动] 核心层] 管理层] 系统服务界面,中断入口(不公开)] 系统DLL(公开)] ——>API
用户进程没有调用内核函数(特权指令)的权限,只有调用API,通过API函数内的“自陷中断”进入“系统态”调用系统函数。或者是“异常中断”出错进入OS运行。
Win32API{
__asm{
push ebp KiSystemService()
mov eax, 151 //系统函数号 { 中断,保护CPU环境,进入系统态
lea edx, 9[ebp] //用户区参数栈地址 copy栈参数块到系统内存
int 0x2e //中断id,进入IDT表对应函数 ——> 执行eax寄存器里的id调用函数
pop ebp 返回结果,copy回用户区
ret 10 //返回结果参数,9+1 关中断,基本是开头的逆操作 }
}
} //如果用户参数少,可以直接在寄存器中传,用户指定fastcall方式
除外部IO外,自陷和异常也是IDT表中的中断项(255个,外设中断预留100个左右),自陷中断有:
int 0x3 (2c\ 2d) KiTrap3() ,DEBUG调试用
0x2e KiSystemService() ,系统函数调用
0x2b 内核可以调用用户空间的子程序,子程序执行完再返回内核,就用这个中断
0x2a 获取精确CPU时间,轻型调用,普通中断开销大,无法取到精确的时间
通常的特权指令调用都通过KiSystemService() ,通过系统函数调用号id跳转到函数地址上。Windows核心和Liunx类似,有200多个核心函数,但MS又把视窗界面函数也移入内核中。
另外:不同的用户进程,有各自的系统堆栈空间,供其调用的内核函数使用(是彼此独立的)。
内存管理
内存里有系统区,有各个独立的用户区:4G线性内存空间分两半,地址也是由两个16位“段+位移”组合。保护模式下,用户区无法设置高位段地址,它一直是0,故永远无法访问系统区。
内存里有代码段,数据段:全局量在编译时就静态的在用户进程独立的线性地址空间分配了固定地址;各线程有各自独立的栈空间;malloc在堆分配,程序结束会释放,但对于作为服务器的程序,一直有内存泄漏是个问题。
进程 —— 线性地址空间 <—— 页面映射表 ——> 物理内存页 <——> 硬盘虚拟内存页
用户进程间的空间相互独立。但也有些页面可以被多个进程共享,供进程间通信,这就是“共享映射区”和“文件映射区”。
用户进程有“堆”,内核有“池”。用户的堆可扩充,需要时分配,只要不超过用户运行空间。内核的池是全局的,属于整个系统,一般固定大小,只供内核使用,有的可以扩充,这意味着参与了虚拟内存页的倒换。有系统函数来申请这些可扩充池的缓冲区。
对象管理
Windows把一些核心功能作为内核对象来管理,这些简单的对象是一些结构体,没有封装、继承、多态等高级功能。Windows是基于对象不是面向对象的设计方式。
内核对象有很多,还可以通过.sys模块添加新类型,通过OpenFile()通用方式打开对象获得句柄,但底下具体的实现因对象类型不同而异。
OS有个对象类型目录,表结构,存放指向OS里注册的对象类型的指针。对象类型也是struct。
对象数据结构:[object_body|object_head|object_info],head里没有挂入队列的链指针。即对象自身并没要求要加入对象目录,它可以是独立的,提供句柄供用户进程控制。但被多个进程共享,或是被一个进程重复“创建”的对象,在OS中可以通过且只能命名找到它,OS将命名对象加入了对象目录来便于查找。对象目录是树结构,根节点是目录对象(系统内部用struct),普通对象都是叶节点。节点之间通过连接对象连接(普通对象没有链指针),目录节点通过Hash表、对象名来组织叶节点。
打开模式:创建对象,初始化,或已有对象引用计数加1,建立这一次打开的上下文(运行信息:访问权限,占据内存页,换入换出设置等,因具体类型而不同);加入对象目录和进程句柄表,返回句柄表的索引,即Handle,从而建立进程与对象的连接。
每个进程有一个句柄表,表项指向对象的head。有时进程通过一些函数会直接操作内核对象,这时候要挂靠内核句柄表(内核运行也在使用一些对象),copy修改一些句柄值。多个进程的句柄表可以指向同一个对象。
进程与线程也是对象,一个进程创建一个子进程,也就有了这个进程的句柄。
消息(视窗报文)
视窗是Windows支持的一个子系统,它把原先在用户空间实现的图形界面移入了内核,主要包括人机界面绘图GUI,和视窗间通信。
视窗间通信就是线程间通信,视窗线程是扩充了的普通线程,增加了线程系统堆栈空间,因为视窗操作的嵌套深度远远超过普通的内核线程。新增一个struct{ 本线程消息队列 },每个视窗线程有一个消息队列。
一个进程,只要有一个线程是视窗线程,这个进程就是视窗进程,新增一个struct{ KeyboardLayout//键盘格式; GDIObject//界面绘图对象; UserObject; }
一个视窗程序,主进程,主线程
WinMain()
{
wndclass; wndclass.ClassName; wndclass.样式; wndclass.lpWndProc;
RegisterClassEx(&wndclass); //注册视窗对象类型
hWnd = CreateWindow("ClassName","title",Rect,hInstance,…); //产生一个视窗对象——
ShowWindow(); UpdateWindow(); |
while(GetMessage()) |
{ Translate(); //将键盘字符换为Unicode |
DispatchMessage(&msg); } //主线程把消息分送各视窗队列 |
} |
Window_Object{threadinfo; OwnerThread; 所属线程的消息队列地址; hDesktop; "对象名"; Rect;
child;prev;next; Parent;Owner;WndProc}
Window_Object视窗对象在内核中,是全局的。不是属于进程的对象。不同进程的hWnd不可能有相同值,如果不是同一个对象的话。在OS全局句柄表中。
一般一个线程控制一个独立的主视窗,主视窗创建的子视窗也在同一线程中。一些非阻塞做一些独立功能的视窗新开一个线程,也不依附于主视窗(如进度条)。
一个线程有一个消息队列,一个GetMessage\DispatchMessage操作,分送消息给多个视窗各自的WndProc函数。
具体的消息收发,是由OS Kerner视窗子系统的报文通信服务进程控制的。这是一个全局的服务进程,OS根据当前硬件状态(鼠标、键盘内核线程;点击、拖拽、输入),当前激活的进程(视窗、子控件),当前视窗的状态(选中边缘、内框、菜单)等综合考虑,决定当前发送的消息类型,Msg对象,Msg参数,放入进程的消息队列。进程循环GetMsg后,没有消息挂起休眠,有消息DispatchMessage(),API,由内核实现,在底层又调用用户空间的hWnd->WndProc();处理Msg。
消息来源:1、视窗子系统服务进程监视硬件产生的;2、来自别的进程、线程或自身线程主动地SendMessage()、PostMessage()。
GetMessage(lpMsg, NULL, 0, 0) //NULL则是不限视窗,都获取
{ OSGetMessage(&Msg, this.hWnd//NULL, 0//MsgFilterMin, 0//MsgFilterMax)
//从进程系统区消息队列获取消息,需要指出是进程哪个hWnd的消息
KMToUMMsg(); //从内核模式的消息转为用户模式
}
消息的数据结构{ hWnd; UINTmessage; 参数w; 参数p; time; point; } 参数是void*,可以带一大块数据。内核模式转为用户模式有一个工作就是将参数数据考入用户区(不然进程无法访问)。
线程向别的线程或自身发送消息,也是将消息挂入目标线程消息队列。Post方式,挂入就可返回。Send方式至少要等目标确认(对消息作出反应),当然不会象网络通信那样发送握手消息,而是在自身消息队列也挂入一个消息,然后睡眠等待己方消息执行完。对方处理了这个消息后,会把己方消息也消掉。这是由OS控制的。
回调函数:一般是用户进程通过中断调用内核函数,内核函数在API机制下,执行完后返回用户空间,也会调一些用户函数。回调函数是指内核函数执行时,调用一些用户事先定义的函数,执行完再返回内核继续运行。这一套机制在OS内部也需要专门实现。这些内核函数预先留了挂钩,对应在可能执行这些内核函数的用户进程里,也有这些挂钩的接口,即一个回调函数表。不同进程(窗体进程、普通进程)可以有不同的回调函数表。挂钩的形式是索引号,类似于中断向量,对应也有中断操作地址,用户在这些预留地址指针上定义回调函数体。DispatchMessage — OS —> WndProc 就是在内核调用了用户进程里的消息处理函数。
HOOK钩子:只要是通过视窗Msg传递的消息,OS已提供了方法在中途挂“钩子”拦截消息,在 DispatchMessage — OS —> WndProc 的中间OS部分,将消息引出到用户空间的外挂函数(这也是内核预留了HOOK操作的挂钩)。
视窗消息,除了少数来自进程Send\Post的消息,大多来自IO设备,而绝大部分来自键盘和鼠标。
对于Windows视窗子系统,控制多进程、多视窗,不能让线程各自独立的读取IO输入,和各自为政的在屏幕上乱画。这样要统管这些相互影响的进程(窗体重叠,判断鼠标隶属),要在用户区进程间进行大量消息传递。于是在内核建立了一个统一的进程管理键盘鼠标输入,另一个进程管理GUI桌面绘图。
键盘线程:KeyThreadMain(),一直循环读取设备文件\Device\KeyboardClass0,读入键盘驱动程序传上来的扫描码,按键状态(按下、弹起)。还提供了回调函数挂钩,供用户区输入法软件解析读入字符。如果键盘有这些输入,将其组合成消息,WM_KEYDOWN、WM_KEYUP,参数l指明扫描码值,参数w要指明消息发送对象,即当前激活的视窗所属的线程。这在OS进\线程管理器中很容易提供。内核会把键盘消息Post到激活视窗线程的消息队列,只要它没被HOOK钩子拦截。
鼠标线程:设备为\Device\PointerClass0 。鼠标记录了各个按键状态,和位置移动,这些都有其标志位表示。屏幕光标跟随鼠标移动,这也是在内核中实现,发生在WM_MOUSEMOVE消息提交给目标窗体之前。鼠标消息根据操作指定类型,参数表示按键状态、移动状态和当前位置。鼠标消息没有目标视窗,它首先放入的是系统消息队列,而不是某个目标窗体。然后系统根据它的位置判断它落入那个窗体,是在执行什么操作,可能以此产生多个绘图消息(窗体隐藏、拉大、缩小)。鼠标操作很多时候会影响到窗体绘图,相应键盘事件影响较少,除了系统HotKey或菜单快捷键,所以键盘消息在发送目标窗体前也要由内核比对一下HotKey表,或发送后在进程里,再引起绘图事件。鼠标事件预处理后,再加入合适的激活线程消息队列。
用户进程未处理的消息,又交给系统进程DefWndProc处理,以保证不遗漏消息。
进程管理
进程相当于一个虚拟单任务计算机,有一个地址空间,一套寄存器,一批自己的打开文件或内核对象。而实际上是多个进程共享一台物理计算机,所以每个进程才走走停停,进程的“上下文”和临界互斥量才那么重要。
线程这是进程里的计算绪,也有上下文,只与寄存器相关。