⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 2 页
字号:
漫谈兼容内核之四: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,

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -