漫谈兼容内核之四:Kernel-win32的进程管理
[align=center][size=5][b]漫谈兼容内核之四:Kernel-win32的进程管理[/b][/size][/align]
[align=center]毛德操[/align]
由于进程管理与对象管理不可分割,我在谈论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]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() */[/code]
不妨假定这是个新创建的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() */[/code]
显然,正是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();
……
}[/code]
这里,测试进程的第一个线程通过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
);[/code]
这是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
)[/code]
详细说明这些参数的作用是件颇费篇幅的事,读者可以自己阅读“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)[/code]
别的就不说了,光是上面这些,读者就可以看出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
);[/code]
同样,这只是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
)[/code]
同样,详细的说明请看“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)[/code]
读者不难看出,这与fork()的差距可真够大的了。而且,有些差异是不能在用户空间弥补的,例如为别的进程创建线程,还有让新创建的线程进入“挂起”状态等等就是这样。
这还只是NtCreateProcess()和NtCreateThread()两个系统调用。与进程/线程管理有关的Windows系统调用至少还有下面这些:
[code]NtAlertThread()
NtCreateProcess()
NtCreateThread()
NtDuplecateObject()
NtGetContextThread()
NtImpersonateThread()
NtOpenProcess()
NtOpenThread()
NtQueueApcThread()
NtResumeThread()
NtSetContextThread()
NtSetInformationProcess()
NtSetInformationThread()
NtSetThreadExecutionState()
NtSuspendThread()
NtTerminateProcess()
NtTerminateThread()
NtYieldExecution()[/code]
由此可见,要跟Windows高度兼容,可真是路慢慢其修远。不过也不要被吓倒,还是那句老话:战略上藐视困难,战术上重视困难。再说也确实并非所有特性都必须加以实现,因为绝大多数应用软件都不会去用那些刁钻古怪的功能。
但是有一点是明白无误的,那就是迄今为止的Kernel-win32还只是朝正确的方向走了一小步。而且,其设计方案也有不当之处,想要用fork()加Win32Init()实现进程/线程的创建就是。这也是我认为对于兼容内核而言ReactOS远比Kernel-win32重要的原因。