漫谈兼容内核之四:Kernel-win32的进程管理
2009-03-13 00:55
351 查看
由于进程管理与对象管理不可分割,我在谈论Kernel-win32的对象管理时也谈到了一些有关进程管理的内容,例如对task_struct数据结构的扩充,以及对Linux内核有关代码所打的补丁。但是这还不够,还需要进一步讨论。
对于任何现代操作系统而言,进程(线程)管理都是一个十分重要的环节。Windows与Linux在这方面恰恰有着相当大的差异,有的是概念上的,有的是实现细节上的:
1. 在Linux内核中,线程和进程都是由task_struct数据结构作为代表的。一个task_struct数据结构所代表的实体,只要是与其父进程共享同一用户空间的就是线程;否则,如果已经“另立门户”、拥有自己的用户空间,那就是进程。或者,如果换一种观点,那就是进程及其“第一个线程”是合一的,是同一回事。在Linux内核中,task_struct数据结构就是进程调度的单位。而在Windows中,则进程与线程有不同的数据结构,只有代表着线程的数据结构才是调度的单位,而代表着进程的数据结构是被架空的,没有受调度运行的权利。因此,所谓创建一个Windows进程,总是意味着创建一个进程及其“第一个线程”,以两个不同数据结构的组合作为代表。进程与线程是一对多的关系,这在Linux中和Windows中都一样,但是在Linux中这体现为一组task_struct数据结构的“家属树”,逻辑上是层次结构,实现上则是网状结构(属于同一进程的同层线程之间也有链接)。而在Windows中则体现为一个进程结构和多个线程结构,最自然的当然是让所有的线程排成一个队列,并且都有指针指向其所属进程的数据结构。
2. 抛开在结构形态上的不同,Linux的task_struct结构也并不是简单地把Windows的进程结构和线程结构加在一起。有些成分在Windows的数据结构中有,而在task_struct结构中没有,有些则反过来。
3. 两个系统用于创建进程/线程的系统调用在语义上有很大的区别。在Linux中,这首先是父进程的“细胞分裂”,即分裂成两个线程,然后如果子进程另立门户就又变成两个进程。就是说,创建线程是创建进程的必经之途。而在Windows中,则创建进程和创建线程是两码事,创建进程的系统调用并不蕴含着同时创建其第一个线程。
4. 进程在两个系统中的地位与权利有很大区别。在Linux中每个进程都有相当的独立性,有自己的“隐私”和“私有财产”,而在Windows中一个进程甚至可以替另一个进程创建一个线程。
5. 两个系统在资源和权限的遗传/继承方面有很重要的区别。
6. 两个系统在调度策略和优先级的设置方面也有区别。在Linux中,由于task_struct是调度单位,每个线程都可以有自己的调度策略和优先级。而在Windows中,则首先是进程一级的优先级,然后是线程在同一进程中的相对优先级。前者是一种水平的结构,后者是一种层次的结构。
7. 两个系统在进程间通信方面也有区别,有的是名称和实现细节的不同,有的确有实质的区别,例如Windows的跨进程复制Handle,就在Linux中没有对应的机制。
显然,要在Linux内核上运行Windows软件,就必须让Windows线程借用Linux的task_struct数据结构,否则就不能被调度运行(要不然就得大改Linux内核中的schedule()了,这当然是应该避免的)。这样,内核中的Windows线程就成为Linux进程/线程的一个子集,或者说特殊的Linux进程/线程。为此,为了在内核中弥补上述的种种不同,首先当然要在task_struct结构中增加一个指针(Kernel-win32使用task_ornament队列),使其指向补充性的附加数据结构。同时,由于要在Linux内核上运行Windows线程,就有个如何确定一个Linux进程是否Windows线程的问题。当然,只要task_struct结构中的附加数据结构指针非0、或队列非空,就说明是个Windows线程。可是,什么时候为其分配附加数据结构并设置这个指针或队列呢?显然这里需要有个依据、有个手段。我们先看Kernel-win32所采用的办法。
Kernel-win32要求所有Windows线程在初始化时都执行一个系统调用Win32Init(),让内核知道当前线程是个Windows线程。这个系统调用是Kernel-win32加出来的,Windows并没有这么一个系统调用。我们先看这个Kernel-win32系统调用的实现:
CODE:
CODE:
以前讲过,其实task_struct数据结构的task_ornament队列中只有一个线程,只不过属于同一个Windows进程的所有线程都通过另一个队列串在一起。第一个线程与后续线程的区别只是:创建第一个线程时要创建新的进程对象(及其线程队列),同一进程中后来创建的线程则不创建进程对象,而只是找到其所属的已有进程对象。
既然新进程(线程)在创建之初时是Linux进程,可想而知新进程(线程)的创建可以通过Linux系统调用实现。事实正是如此,Kernel-win32并没有实现创建进程或线程的Windows系统调用,而仍沿用fork()、execve()等等作为创建进程或线程的手段。Kernel-win32代码中的一些测试程序清楚地表明了这一点,下面是测试程序fivemutex.c中的一些代码。
CODE:
现在可以讨论了。
首先是把对于Win32Init()的调用放在哪里。当然不能放在Windows应用软件中,因为那都是“木已成舟”的二进制可执行映像。比较可行的是放在某个DLL中,最大的可能是放在ntdll.dll中。
然后是什么时候调用Win32Init()。读者可能会想,当应用软件向下调用创建Windows进程或线程的时候,在ntdll.dll中可以先调用fork(),再调用Win32Init()。然而这是错的,因为调用fork()的是父进程(线程),而需要调用Win32Init()的是新创建出来的线程,这是两码事。显然,这里需要某种机制,虽然并非不能实现,却也不是很简单。事实上我们在kernel-win32的代码中尚未见到相应的实现。
更重要的是,用fork()加Win32Init()是否能忠实地实现Windows中那些创建进程/线程系统调用的语义?为此,我们看一下两个Windows系统调用的函数定义。
先看进程的创建。
CODE:
CODE:
首先当然是InheritObjectTable。这是个布尔量,表示是否要从父进程继承已经打开的对象,但是即使要继承也不是全盘照收,还要看具体对象在打开时是否允许遗传。
另一个参数ParentProcess是个已打开进程对象的Handle,如果是有效Handle的话就表示新创建的对象应该“过继”给这个进程、作为它的子进程,而不是作为其创建者即“生父”的子进程。或者,换句话说就是包办、替别的进程创建一个子进程。
而DesiredAccess,则有下列选项:
CODE:
还要注意,这个系统调用只创建进程、而不包括其第一个线程。这跟Win32 API函数CreateProcess()是不一样的,后者实际上先调用NtCreateProcess(),再调用NtCreateThread()。
那么是否可以通过在用户空间、即在DLL中把CreateProcess()化解成一个kernel-win32和Linux系统调用的序列来解决问题呢?有的可以,有的不行。例如上述把所创建的进程过继给另一个进程就不行,因为过继给另一个进程也意味着从“继父”那里、而不是从“生父”那里、继承已打开对象(如果需要的话)。
再看线程的创建。
CODE:
CODE:
CODE:
这还只是NtCreateProcess()和NtCreateThread()两个系统调用。与进程/线程管理有关的Windows系统调用至少还有下面这些:
CODE:
但是有一点是明白无误的,那就是迄今为止的Kernel-win32还只是朝正确的方向走了一小步。而且,其设计方案也有不当之处,想要用fork()加Win32Init()实现进程/线程的创建就是。这也是我认为对于兼容内核而言ReactOS远比Kernel-win32重要的原因。
对于任何现代操作系统而言,进程(线程)管理都是一个十分重要的环节。Windows与Linux在这方面恰恰有着相当大的差异,有的是概念上的,有的是实现细节上的:
1. 在Linux内核中,线程和进程都是由task_struct数据结构作为代表的。一个task_struct数据结构所代表的实体,只要是与其父进程共享同一用户空间的就是线程;否则,如果已经“另立门户”、拥有自己的用户空间,那就是进程。或者,如果换一种观点,那就是进程及其“第一个线程”是合一的,是同一回事。在Linux内核中,task_struct数据结构就是进程调度的单位。而在Windows中,则进程与线程有不同的数据结构,只有代表着线程的数据结构才是调度的单位,而代表着进程的数据结构是被架空的,没有受调度运行的权利。因此,所谓创建一个Windows进程,总是意味着创建一个进程及其“第一个线程”,以两个不同数据结构的组合作为代表。进程与线程是一对多的关系,这在Linux中和Windows中都一样,但是在Linux中这体现为一组task_struct数据结构的“家属树”,逻辑上是层次结构,实现上则是网状结构(属于同一进程的同层线程之间也有链接)。而在Windows中则体现为一个进程结构和多个线程结构,最自然的当然是让所有的线程排成一个队列,并且都有指针指向其所属进程的数据结构。
2. 抛开在结构形态上的不同,Linux的task_struct结构也并不是简单地把Windows的进程结构和线程结构加在一起。有些成分在Windows的数据结构中有,而在task_struct结构中没有,有些则反过来。
3. 两个系统用于创建进程/线程的系统调用在语义上有很大的区别。在Linux中,这首先是父进程的“细胞分裂”,即分裂成两个线程,然后如果子进程另立门户就又变成两个进程。就是说,创建线程是创建进程的必经之途。而在Windows中,则创建进程和创建线程是两码事,创建进程的系统调用并不蕴含着同时创建其第一个线程。
4. 进程在两个系统中的地位与权利有很大区别。在Linux中每个进程都有相当的独立性,有自己的“隐私”和“私有财产”,而在Windows中一个进程甚至可以替另一个进程创建一个线程。
5. 两个系统在资源和权限的遗传/继承方面有很重要的区别。
6. 两个系统在调度策略和优先级的设置方面也有区别。在Linux中,由于task_struct是调度单位,每个线程都可以有自己的调度策略和优先级。而在Windows中,则首先是进程一级的优先级,然后是线程在同一进程中的相对优先级。前者是一种水平的结构,后者是一种层次的结构。
7. 两个系统在进程间通信方面也有区别,有的是名称和实现细节的不同,有的确有实质的区别,例如Windows的跨进程复制Handle,就在Linux中没有对应的机制。
显然,要在Linux内核上运行Windows软件,就必须让Windows线程借用Linux的task_struct数据结构,否则就不能被调度运行(要不然就得大改Linux内核中的schedule()了,这当然是应该避免的)。这样,内核中的Windows线程就成为Linux进程/线程的一个子集,或者说特殊的Linux进程/线程。为此,为了在内核中弥补上述的种种不同,首先当然要在task_struct结构中增加一个指针(Kernel-win32使用task_ornament队列),使其指向补充性的附加数据结构。同时,由于要在Linux内核上运行Windows线程,就有个如何确定一个Linux进程是否Windows线程的问题。当然,只要task_struct结构中的附加数据结构指针非0、或队列非空,就说明是个Windows线程。可是,什么时候为其分配附加数据结构并设置这个指针或队列呢?显然这里需要有个依据、有个手段。我们先看Kernel-win32所采用的办法。
Kernel-win32要求所有Windows线程在初始化时都执行一个系统调用Win32Init(),让内核知道当前线程是个Windows线程。这个系统调用是Kernel-win32加出来的,Windows并没有这么一个系统调用。我们先看这个Kernel-win32系统调用的实现:
CODE:
int InitialiseWin32(struct WineThread *thread, struct WiocInitialiseWin32 *args) { struct WineThreadConsData wtcd; …… /* allocate a Wine process object */ probj = AllocObject(&process_objclass,NULL,NULL); …… /* allocate a Wine thread object */ wtcd.wtcd_task = current; wtcd.wtcd_process = probj; throbj = AllocObject(&thread_objclass,NULL,&wtcd); …… return 0; } /* end InitialiseWin32() */不妨假定这是个新创建的Windows进程,从而当前线程是这个进程中的第一个线程。先为之分配和创建一个进程对象(及其配套的WineProcess数据结构)。代码中的数据结构wtcd只是个临时用来传递信息的载体,注意其成分wtcd_task设置成current,这就是指向当前task_struct数据结构的指针。显然,对于新创建的Windows进程,这个结构中的task_ornament队列是空的,所以此刻的当前进程(线程)还是个Linux进程(线程)。接着再分配和创建一个线程对象(及其配套的WineThread数据结构)。我们知道,在创建对象的过程中要调用该类对象的构建函数,对于线程对象就是ThreadConstructor(),我们再重温一下:
CODE:
static int ThreadConstructor(Object *obj, void *data) { struct WineThreadConsData *wtcd = data; …… thread->wt_task = wtcd->wtcd_task; …… add_task_ornament(thread->wt_task,&thread->wt_ornament); …… } void add_task_ornament(struct task_struct *tsk, struct task_ornament *orn) { ornget(orn); write_lock(&tsk->alloc_lock); list_add_tail(&orn->to_list,&tsk->ornaments); write_unlock(&tsk->alloc_lock); } /* end add_task_ornament() */显然,正是ThreadConstructor()把新进程的第一个线程挂入了当前task_struct结构中的task_ornament队列,使其变成非空,从而使Linux进程(线程)变成了Windows线程。至于前面创建的进程对象,那是通过另一个队列跟其所有的线程串在一起的,与task_struct结构并没有直接的连系,这以前已经讲过了。而且,由于每个线程都有自己的task_struct数据结构,实际上每个Windows线程都得在初始化时调用Win32Init()。Kernel-win32似乎并没有考虑“龙生龙,凤生风”式的遗传。
以前讲过,其实task_struct数据结构的task_ornament队列中只有一个线程,只不过属于同一个Windows进程的所有线程都通过另一个队列串在一起。第一个线程与后续线程的区别只是:创建第一个线程时要创建新的进程对象(及其线程队列),同一进程中后来创建的线程则不创建进程对象,而只是找到其所属的已有进程对象。
既然新进程(线程)在创建之初时是Linux进程,可想而知新进程(线程)的创建可以通过Linux系统调用实现。事实正是如此,Kernel-win32并没有实现创建进程或线程的Windows系统调用,而仍沿用fork()、execve()等等作为创建进程或线程的手段。Kernel-win32代码中的一些测试程序清楚地表明了这一点,下面是测试程序fivemutex.c中的一些代码。
CODE:
int main() { int loop; for (loop=0; loop<5; loop++) { switch (fork()) { case -1: ERR(1,"fork"); case 0: return child(loop); default: break; } } while (wait(&loop)>0) {} return 0; } int child(int pid) { HANDLE left, right, first, second; const char *lname, *rname; int count = 0; int wt; Win32Init(); …… }这里,测试进程的第一个线程通过Linux系统调用fork()创建出5个线程,每个线程都执行child()。而所创建的每个线程,则都调用Win32Init(),使其自身变成Windows线程。有趣的是这里的第一个线程main()并没有调用Win32Init(),这是因为它干的尽是Linux的事,所以并不在乎。在这种情况下,fork()出来的第一个线程就成为“Windows进程”的第一个线程,即负有创建进程对象的责任。
现在可以讨论了。
首先是把对于Win32Init()的调用放在哪里。当然不能放在Windows应用软件中,因为那都是“木已成舟”的二进制可执行映像。比较可行的是放在某个DLL中,最大的可能是放在ntdll.dll中。
然后是什么时候调用Win32Init()。读者可能会想,当应用软件向下调用创建Windows进程或线程的时候,在ntdll.dll中可以先调用fork(),再调用Win32Init()。然而这是错的,因为调用fork()的是父进程(线程),而需要调用Win32Init()的是新创建出来的线程,这是两码事。显然,这里需要某种机制,虽然并非不能实现,却也不是很简单。事实上我们在kernel-win32的代码中尚未见到相应的实现。
更重要的是,用fork()加Win32Init()是否能忠实地实现Windows中那些创建进程/线程系统调用的语义?为此,我们看一下两个Windows系统调用的函数定义。
先看进程的创建。
CODE:
CreateProcessA( IN LPCSTR lpApplicationName, IN LPSTR lpCommandLine, IN LPSECURITY_ATTRIBUTES lpProcessAttributes, IN LPSECURITY_ATTRIBUTES lpThreadAttributes, IN BOOL bInheritHandles, IN DWORD dwCreationFlags, IN LPVOID lpEnvironment, IN LPCSTR lpCurrentDirectory, IN LPSTARTUPINFOA lpStartupInfo, OUT LPPROCESS_INFORMATION lpProcessInformation );这是Win32 API界面上的函数定义,所以是个DLL函数,还不是系统调用。Windows系统调用的界面是不公开的。不过好在我们已经有了ReactOS,从ReactOS的代码中可以看到这个系统调用的函数定义是:
CODE:
NtCreateProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN HANDLE ParentProcess, IN BOOLEAN InheritObjectTable, IN HANDLE SectionHandle OPTIONAL, IN HANDLE DebugPort OPTIONAL, IN HANDLE ExceptionPort OPTIONAL )详细说明这些参数的作用是件颇费篇幅的事,读者可以自己阅读“Windows NT/2000 Native API Reference”第六章中关于ZwCreateProcess()的说明(ZwCreateProcess()和NtCreateProcess()是同一个函数的两个名字,有的文献说在用户空间叫ZwCreateProcess()、在内核中叫NtCreateProcess())。我们这里只是长话短说,提一下往往会使Linux程序员感到惊讶的东西。
首先当然是InheritObjectTable。这是个布尔量,表示是否要从父进程继承已经打开的对象,但是即使要继承也不是全盘照收,还要看具体对象在打开时是否允许遗传。
另一个参数ParentProcess是个已打开进程对象的Handle,如果是有效Handle的话就表示新创建的对象应该“过继”给这个进程、作为它的子进程,而不是作为其创建者即“生父”的子进程。或者,换句话说就是包办、替别的进程创建一个子进程。
而DesiredAccess,则有下列选项:
CODE:
#define PROCESS_TERMINATE 1 #define PROCESS_CREATE_THREAD 2 #define PROCESS_SET_SESSIONID 4 #define PROCESS_VM_OPERATION 8 #define PROCESS_VM_READ 16 #define PROCESS_VM_WRITE 32 #define PROCESS_DUP_HANDLE 64 #define PROCESS_CREATE_PROCESS 128 #define PROCESS_SET_QUOTA 256 #define PROCESS_SET_INFORMATION 512 #define PROCESS_QUERY_INFORMATION 1024 #define PROCESS_ALL_ACCESS / (STANDARD_RIGHTS_REQUIRED|SYNCHRONIZE|0xFFF)别的就不说了,光是上面这些,读者就可以看出NtCreateProcess()与fork()、execve()等Linux系统调用的差距有多大了。显然,以目前的kernel-win32,要实现与Windows的高度兼容是不可能的。
还要注意,这个系统调用只创建进程、而不包括其第一个线程。这跟Win32 API函数CreateProcess()是不一样的,后者实际上先调用NtCreateProcess(),再调用NtCreateThread()。
那么是否可以通过在用户空间、即在DLL中把CreateProcess()化解成一个kernel-win32和Linux系统调用的序列来解决问题呢?有的可以,有的不行。例如上述把所创建的进程过继给另一个进程就不行,因为过继给另一个进程也意味着从“继父”那里、而不是从“生父”那里、继承已打开对象(如果需要的话)。
再看线程的创建。
CODE:
CreateThread( IN LPSECURITY_ATTRIBUTES lpThreadAttributes, IN DWORD dwStackSize, IN LPTHREAD_START_ROUTINE lpStartAddress, IN LPVOID lpParameter, IN DWORD dwCreationFlags, OUT LPDWORD lpThreadId );同样,这只是API函数,而相应的系统调用是NtCreateThread(),下列函数定义取自ReactOS的代码:
CODE:
NtCreateThread( OUT PHANDLE T hreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN HANDLE ProcessHandle, OUT PCLIENT_ID ClientId, IN PCONTEXT ThreadContext, IN PINITIAL_TEB InitialTeb, IN BOOLEAN CreateSuspended )同样,详细的说明请看“Windows NT/2000 Native API Reference”第五章,这里只是简单地提一下。首先是ProcessHandle,这是个已打开进程对象的Handle。这就是说,NtCreateThread()的调用者可以为别的进程创建线程,而不仅仅是为调用者本身所属的进程。再看CreateSuspended,这是个布尔量,表示新创建的线程是否一出生就先被挂起、等到有别的线程对其执行NtResumeThread()后才投入运行,抑或一生下来就立即投入运行。还有ThreadContext,这是个指针,可以指向一个数据结构,里面规定了新线程降生之初各个寄存器的值(真令人难以理解这到底是为什么)。还有参数InitialTeb,在“Native API”一书中说是UserStack,用来指定新线程的用户空间堆栈的位置(这倒有道理)。至于DesiredAccess,则又有下列许多选项:
CODE:
#define THREAD_TERMINATE (0x0001L) #define THREAD_SUSPEND_RESUME (0x0002L) #define THREAD_ALERT (0x0004L) #define THREAD_GET_CONTEXT (0x0008L) #define THREAD_SET_CONTEXT (0x0010L) #define THREAD_SET_INFORMATION (0x0020L) #define THREAD_QUERY_INFORMATION (0x0040L) #define THREAD_SET_THREAD_TOKEN (0x0080L) #define THREAD_IMPERSONATE (0x0100L) #define THREAD_DIRECT_IMPERSONATION (0x0200L) #define THREAD_ALL_ACCESS (0x1f03ffL)读者不难看出,这与fork()的差距可真够大的了。而且,有些差异是不能在用户空间弥补的,例如为别的进程创建线程,还有让新创建的线程进入“挂起”状态等等就是这样。
这还只是NtCreateProcess()和NtCreateThread()两个系统调用。与进程/线程管理有关的Windows系统调用至少还有下面这些:
CODE:
NtAlertThread() NtCreateProcess() NtCreateThread() NtDuplecateObject() NtGetContextThread() NtImpersonateThread() NtOpenProcess() NtOpenThread() NtQueueApcThread() NtResumeThread() NtSetContextThread() NtSetInformationProcess() NtSetInformationThread() NtSetThreadExecutionState() NtSuspendThread() NtTerminateProcess() NtTerminateThread() NtYieldExecution()由此可见,要跟Windows高度兼容,可真是路慢慢其修远。不过也不要被吓倒,还是那句老话:战略上藐视困难,战术上重视困难。再说也确实并非所有特性都必须加以实现,因为绝大多数应用软件都不会去用那些刁钻古怪的功能。
但是有一点是明白无误的,那就是迄今为止的Kernel-win32还只是朝正确的方向走了一小步。而且,其设计方案也有不当之处,想要用fork()加Win32Init()实现进程/线程的创建就是。这也是我认为对于兼容内核而言ReactOS远比Kernel-win32重要的原因。
相关文章推荐
- 漫谈兼容内核之四:Kernel-win32的进程管理
- 漫谈兼容内核之二:关于kernel-win32的对象管理
- 漫谈兼容内核之二:关于kernel-win32 的对象管理
- 漫谈兼容内核之二:关于kernel-win32的对象管理
- 漫谈兼容内核之三:Kernel-win32的文件操作
- 漫谈兼容内核之五:Kernel-win32的系统调用机制
- 漫谈兼容内核之三:Kernel-win32的文件操作
- 漫谈兼容内核之五:Kernel-win32的系统调用机制
- 漫谈兼容内核之二十一:Windows进程的用户空间
- 漫谈兼容内核之十七:再谈Windows的进程创建
- 漫谈兼容内核之十三:关于“进程挂靠”
- 漫谈兼容内核之二十一:Windows进程的用户空间
- 漫谈兼容内核之十:Windows的进程创建和映像装入
- 漫谈兼容内核之十三:关于“进程挂靠”
- 漫谈兼容内核之十四:Windows的跨进程操作
- 漫谈兼容内核之十四:Windows的跨进程操作
- 漫谈兼容内核之十七:再谈Windows的进程创建
- 漫谈兼容内核之十:Windows的进程创建和映像装入
- 读薄「Linux 内核设计与实现」(2) - 进程管理和调度
- 漫谈兼容内核之十一:Windows DLL的装入和连接