Chinaunix首页 | 论坛 | 博客
  • 博客访问: 488860
  • 博文数量: 164
  • 博客积分: 4024
  • 博客等级: 上校
  • 技术积分: 1580
  • 用 户 组: 普通用户
  • 注册时间: 2009-10-10 16:27
文章分类

全部博文(164)

文章存档

2011年(1)

2010年(108)

2009年(55)

我的朋友

分类: 嵌入式

2010-06-01 10:28:11

漫谈兼容内核之四: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重要的原因。
阅读(956) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~