您的位置:首页 > 其它

漫谈兼容内核之二:关于kernel-win32的对象管理

2009-03-13 00:09 316 查看
近来屡有网友提到一个旨在将Wineserver移入内核的开源项目kernel-win32;有问及其本身,希望能对其代码作一些分析、讲解的,也有问及兼容内核与此项目之间关系的。所以从这篇漫谈开始就来谈谈kernel-win32。
首先,兼容内核项目应当从所有(能找到的)相关开源项目吸取营养,有时候甚至就采取“拿来主义”,反正都是开源项目,只要遵守有关的规定就行。从这个意义上说,我们对于kernel-win32肯定要借鉴,也可能要“拿来”一些。但是这种借鉴和拿来的取舍必须以客观的分析为基础,必须与我们的终极目标相一致。相信读者在看完从本文开始的几篇漫谈以后就会明白我为什么把Wine、ReactOS、和NDISwrapper列为兼容内核的三个主要源泉,而没有把kernel-win32也列为主要源泉之一。

从总体上说,kernel-win32把原来由Wine服务进程提供的某些功能和机制移入了Linux内核,具体(就目前所见版本而言)有这么一些:
1. 文件操作。
2. Semaphore操作。
3. Mutex操作。
4. Event操作。
5. 作为同步手段的WaitForMultipleObjects()系统调用。
所有这些机制和功能的实现都有个共同的基础,那就是各种内核“对象(Object)”及其Handle的实现。由于已打开的对象是属于进程的资源,又由同一进程中的所有线程所共享,所以又跟进程、线程的实现和管理有关。
此外,kernel-win32也提供了比Wine更高效的RPC机制,用以提高应用进程与Wine服务进程通信的效率。
但是kernel-win32的实现并不完整,甚至并不构成局部的完整性,而且已经实现的部分恰恰是相对而言难度并不高的部分,所采取的方案也还值得推敲。
特别地,kernel-win32的目标只在于提高Wine的效率,所以并不涉及设备驱动。与兼容内核的终极目标相比,二者只是在一小段路程上有“同路”的关系。以我们的眼光看,kernel-win32无疑是朝正确的方向上在走,但是走的毕竟太近。

Kernel-win32的代码大体上分成三个部分。第一部分是对于Linux内核代码的补丁,在它的kernel目录下。第二部分是它本身的代码,是要作为module动态安装到内核中去的,具体的代码文件都在它的根目录下。第三部分是一些测试/演示程序,这是作为应用软件在Wine上运行,或者部分地绕过Wine、与Wine并行的,特别是其中还包括一个用于启动系统调用的库程序win32.c,这些程序都在它的test目录下。从逻辑上说,库程序win32.c属于kernel-win32,而测试/演示程序则不是;就好像Linux的内核与libc都属于Linux,而用来测试/演示其功能的程序却不算一样。此外,为了帮助调试,代码中的strace目录下还包括了对Linux的一个调试工具strace的补丁与扩充。这个工具可以用来跟踪应用软件所作的Linux系统调用,实时地显示所跟踪的应用软件进行具体系统调用时的参数和内核的返回值。这对于调试当然大有帮助,但在逻辑上也并不是kernel-win32的一部分。
下面我从kernel-win32的代码入手,对它的方方面面作一简要的介绍和分析,本篇先说kernel-win32的对象管理,即Object和Handle的实现,实际上也必然会牵涉到进程和线程。

Object和Handle的实现
我曾经讲到,Linux把设备看成文件,所以“文件”是个广义的概念;而Windows又进一步把文件看成“对象(Object)”,“对象”是个更广义的概念。而标志着已打开对象的“句柄(Handle)”,则虽然在物理意义上与“打开文件号”相似(实质上都是下标),却有着许多不同的特性,因而不能把二者混为一谈。为了把Linux(内核)的文件系统机制“嫁接”到Windows的系统调用界面,必须为每个运行着Windows应用的进程(下称“Windows进程”或“Wine进程”)准备下一个地方,用来维持一个类似于“打开文件表”的“打开对象表”。另一方面,Windows的“进程”和“线程”都与它们的Linux对应物有所不同,从而有着不同的数据结构,因此需要为每个Windows进程提供附加的数据结构,作为对Linux“进程控制块”、即task_struct数据结构的补充,并且在二者之间建立起某种连接,例如在task_struct结构中增添一个指针等等。
Kernel-win32本质上正是这样做的,我们不妨看一下它对task_struct结构所打的补丁(为便于阅读,已经作了一些整理):

struct task_struct {
. . . . . .
- spinlock_t alloc_lock;
+ rwlock_t alloc_lock;
+ struct list_head ornaments;
};

本来alloc_lock是这个数据结构中的最后一个成分,现在把它的类型从spinlock_t改成了rwlock_t,这是因为原来的加锁只是针对多处理器结构、防止不同处理器之间互相冲突的,现在则范围有所扩大。而所增添的成分,则是一个双链队列头,称为“ornaments”。Ornament这个词原本是“装饰、饰物”的意思,在这里则引伸成“附件、补充”的意思。
那么准备要链入这个队列的数据结构是什么呢?这是task_ornament数据结构:

struct task_ornament {
atomic_t to_count;
struct list_head to_list;
const struct task_ornament_operations *to_ops;
};

其中的to_count显然是个使用计数,计数为0时表示该数据结构不再有“用户”,从而可以撤销了。队列头to_list显然就是用来将此数据结构挂入ornaments队列的,所以这儿实质性的成分就是指针to_ops,它指向一个task_ornament_operations数据结构,里面主要是一些函数指针。目前kernel-win32只定义了一种task_ornament_operations数据结构,即wineserver_ornament_ops,后面还要讲到。
而task_ornament数据结构,则又可以是另一个数据结构WineThread中的一个成分。所以挂入ornaments队列的(除特殊情况外) 实际上都是WineThread数据结构,此时task_ornament数据结构起着“连接件”的作用。

struct WineThread {
#ifdef WINE_THREAD_MAGIC
unsigned wt_magic; /* magic number */
#endif
struct task_ornament wt_ornament; /* Linux task attachment */
struct task_struct *wt_task; /* Linux task */
Object *wt_obj; /* thread object */
struct WineProcess *wt_process; /* Wine process record */
struct list_head wt_list; /* process's thread list */
enum WineThreadState wt_state; /* thread state */
unsigned wt_exit_status; /* thread exit status */
pid_t wt_tid; /* thread ID */
};

当然,每个WineThread数据结构代表着一个Wine线程、即Windows线程。
在Linux内核中,task_struct数据结构代表着一个进程或线程,是内核调度运行的对象。Wine线程既要受调度运行,就必须落实到一个task_struct数据结构、即Linux线程或进程上。反过来,作为一个受调度运行单位的task_struct数据结构(如果代表着Wine线程的话),也不能代表多个Wine线程,否则这几个Wine线程就合并成一个调度单位了。所以这二者之间应该是一一对应的关系。既然是一一对应的关系,就应该使用指针、而不是队列来建立互相的连系。而既然采用了队列,队列中又只能有一个Wine线程,那么队列中别的成员(如果有的话),就必定是别的什么东西了。从逻辑上说,同一个队列中的诸多成员之间有着平等的关系,可是有什么东西可以和线程处于平等的地位呢?所以这是值得推敲的,后面我还要谈这个问题。
WineThread结构中的几个成分需要加以说明:
指针wt_obj指向一个Object指针。在Windows中线程、进程都是“对象”,都要有一个Object数据结构作为代表。对于Object数据结构等一下再作说明。
指针wt_task指向当前进程(线程)的task_struct数据结构,这样就可以在一个Wine线程与其所落实的Linux线程之间建立起双向的连系(另一个方向就是顺着Linux线程的ornaments队列)。另一个指针wt_process指向一个WineProcess数据结构。显然,WineProcess数据结构代表着Widows进程。
在Linux内核中,进程并没有独立于线程的数据结构,都由task_struct作为代表。一个进程初创(通过execve()等调用与其父进程决裂)时就成为进程,同时也可以说是该进程中的第一个线程。以后,该进程通过fork()等调用创建子进程。子进程在创建之初都是与父进程共享空间的,因而都是线程,后来才通过execve()等调用另立门户,有了自己的空间而成为进程。然而Windows不是这样。在Windows中。一个进程与该进程的第一个线程(以及别的线程)是两个不同的概念,有不同的数据结构。大体上说,进程代表着资源,特别是代表着一片用户空间;而线程则代表着上下文。打个比方,进程就像是舞台和剧本,而线程是演员及其表演的过程。在Windows内核中,这两方面的信息分置于不同的数据结构中,而在Linux内核中则全都存放在task_struct结构中。但是Windows进程和线程的有些信息是task_struct结构中所没有或者不同的,这也正是需要在task_struct结构之外加以“装饰”、补充的原因。
WineProcess数据结构的定义如下:

struct WineProcess {
int wp_magic; /* magic number */
struct nls_table *wp_nls; /* unicode-ascii translation */
pid_t wp_pid; /* Linux task ID */
enum WineProcessState wp_state; /* process state */
struct list_head wp_threads; /* thread list */
rwlock_t wp_lock;
struct Object *wp_obj; /* process object */
struct Object *wp_handles[0]; /* handle map */
};

其中的队列头wp_threads与上面WineThread结构中的wt_list相对应,用来构成一个Windows进程与其所含的所有Windows线程之间的双链队列。如前所述,Windows进程在概念上并不落实到某个具体的Linux进程或线程,所以这个数据结构中并没有指向task_struct结构的指针。不过实质上当然还是有连系的,因为在Linux中一个“进程”和它的“第一个线程”是一回事。这里的wp_pid既然说是Linux的“task ID”(就是Linux的pid),实际上还是一样,还不如改成task_struct结构指针更好。
顺便提一下,Windows进程与Linux进程在优先级设置方面有很大的不同,而Linux内核是根据task_struct数据结构中记载的优先级进行调度的,这里面有个如何换算的问题。WineProcess数据结构中没有关于进程优先级的记载,显然kernel-win32的作者还没有考虑这个问题。
在Windows中进程也是“对象”,所以这里也有个Object结构指针wp_obj。这与WineThread结构中的指针wt_obj是一样的,只不过两个对象的类型不同,一个代表着进程,一个代表着线程。
指针数组wp_handles[ ]才是这个数据结构的实质所在,这就是一个Windows进程的“打开对象表”。数组中的每个有效(非0)指针都指向一个Object结构;对象的类型不同,相应Object结构所代表的目标也就不同。例如有的代表着文件,有的代表着“事件”,有的代表着进程,等等。而具体指针在数组中的下标(严格地说是经过换算的下标,见后所述),则就是打开该对象后的Handle。在代码中,这个数组的大小定义为0,这是因为其大小取决于为WineProcess数据结构分配的存储空间的大小。目前,kernel-win32总是为WineProcess数据结构分配一个4KB的物理页面,扣除这个数据结构的头部以后就都用于这个数组,其大小的计算如下所示:

#define MAXHANDLES ((PAGE_SIZE-sizeof(struct WineProcess))/sizeof(struct Object*))

所以“打开对象表”的大小大约是1020。这与Windows相比当然差距很大(在理论上,Windows打开对象表的大小几乎是无限的),但是实际上也够了。
前面我一直说Handle就是下标,其实这只是就其逻辑意义而言,严格地说是经过换算的下标。这是因为:首先,0不是一个合法的handle数值。另一方面,Handle的数值都是4的倍数,所反映的是以字节为单位的位移。如果index是真正意义上的下标,那么handle的数值就是(index+1)* sizeof(Object*)。
总之,每个进程都有一个打开对象表,为该进程所含的诸多线程所共享,表中的每个有效指针都指向一个Object数据结构。内核中的对象就好像磁盘上的文件一样,都有个从创建到打开、到关闭、最后被删除的“生命周期”。每个对象都由一个Object数据结构作为代表,其定义为:

/*
* object definition
* - object namespace is indexed by name and class
*/
typedef struct Object {
struct list_head o_objlist; /* obj list (must be 1st) */
#ifdef OBJECT_MAGIC
int o_magic; /* magic number (debugging) */
#endif
atomic_t o_count; /* usage count */
wait_queue_head_t o_wait; /* waiting process list */
struct ObjectClass *o_class; /* object class */
struct oname o_name; /* name of object */
void *o_private; /* type-specific data */
} Object;

对象的类型是以其数据结构中的o_class为标志的,这个指针指向哪一个ObjectClass数据结构,这个对象就是什么类型。此外,每个对象都可以有个对象名,就好像每个文件都有个文件名一样,o_name就是用来保持对象名的数据结构。
显然,内核中对象的数量可以很大,这里有个如何寻找某个特定对象的问题。所以首先要按对象的类型划分,然后为每个类别都安排若干个以对象名的hash值划分的队列。具体对象的Object数据结构就按其对象名的hash值链入所属类别的某个队列中,结构中的队列头o_objlist就是用于这个目的。
读者不难看出,Object数据结构中各个成分所反映的基本上都是作为某类对象的共性,而并没有反映出具体对象的个性。所以这个结构中有个无类型的指针o_private,用来指向一个描述具体对象的数据结构。当对象的类型为线程时,这就是个WineThread数据结构;当对象的类型为文件时,这就是个WineFile数据结构;如此等等。
如上所述,Object数据结构中的指针o_class标志着对象的类型。这是个指向某个ObjectClass结构的指针,每个ObjectClass结构代表着一种对象类型,其定义如下。

struct ObjectClass {
struct list_head oc_next;
const char oc_type[6]; /* type name (5 chars + NUL) */
int oc_flags;
#define OCF_DONT_NAME_ANON 0x00000001 /* don't name anonymous objects */

int (*constructor)(Object *, void *);
int (*reconstructor)(Object *, void *);
void (*destructor)(Object *);
int (*describe)(Object *, struct wineserver_read_buf *);
int (*poll)(struct wait_table_entry *, struct WineThread *);
void (*detach)(Object *, struct WineProcess *);

/* lock governing access to object lists */
rwlock_t oc_lock;

/* named object hash */
struct list_head oc_nobjs[OBJCLASSNOBJSSIZE];

/* anonymous object list */
struct list_head oc_aobjs;
};

结构中的第一个队列头oc_next用来构成一个对象类型、即ObjectClass数据结构的队列。而队列头数组oc_nobjs[OBJCLASSNOBJSSIZE],则用来构成该对象类的hash队列数组,具体的对象根据其对象名的hash值决定链入其中的哪一个队列。数组的大小OBJCLASSNOBJSSIZE定义为16。对象也可以是无名的,无名的对象都链入所属类别的无名队列、即oc_aobjs队列。
ObjectClass数据结构中最具实质性的成分是一组函数指针,特别是其中的“构造”函数指针constructor,因为它决定了如何构造出一个具体类型的对象。目前keenel-win32定义了event_objclass、file_objclass、mutex_objclass、semaphore_objclass、process_objclass、thread_objclass、rpc_client_objclass、rpc_server_objclass、rpc_service_objclass、section_objclass等11种对象类型的ObjectClass数据结构。
我们不妨以比较简单的“信号量(semaphore)”类型为例来说明对象的创建。
函数CreateSemaphoreA()是kernel_win32对同名系统调用在内核中的实现,其目的是为当前进程创建并打开一个(内核中的)带有对象名的信号量。我们先跳过系统调用如何进入内核这一步,从这个函数开始看有关的代码。

int CreateSemaphoreA(struct WineThread *filp, struct WiocCreateSemaphoreA *args)
{
HANDLE hSemaphore;
Object *obj;

obj = CreateObject(filp,&semaphore_objclass,args->lpName,args, &hSemaphore);
. . . . . .
return (int) hSemaphore;
} /* end CreateSemaphoreA() */

调用参数filp指向当前线程的WineThread数据结构,这是由kernel_win32所实现的系统调用机制所提供的。另一个参数args,则是指向一个WiocCreateSemaphoreA数据结构的指针。Kernel_win32所实现的系统调用机制把Windows系统调用的参数都组装在一个数据结构中,再把这个结构的起始地址作为参数传给内核。为此,Kernel_win32为其所实现的每个Windows系统调用都定义了一个数据结构,WiocCreateSemaphoreA就是为系统调用CreateSemaphoreA()的参数而定义的数据结构。
CreateSemaphoreA()的主体就是对函数CreateObject()的调用。由于要创建的对象是信号量,就把此种对象类型的数据结构semaphore_objclass的起始地址也作为参数传了下去。参数&hSemaphore则是用来返回Handle的。

[CreateSemaphoreA() > CreateObject()]

Object *CreateObject(struct WineThread *thread, struct ObjectClass *clss,const char *name, void *data, HANDLE *hObject)
{
struct WineProcess *process;
struct oname oname;
Object *obj, **ppobj, **epobj;
int err;

*hObject = NULL;

/* retrieve the name */
err = fetch_oname(&oname,name);
if (err<0)
return ERR_PTR(err);

/* allocate an object */
obj = _AllocObject(clss,&oname,data);
if (oname.name) putname(oname.name);
if (IS_ERR(obj))
return obj;

/* find a handle slot */
process = GetWineProcess(thread);
epobj = &process->wp_handles[MAXHANDLES];
write_lock(&process->wp_lock);
for (ppobj=process->wp_handles; ppobj<epobj; ppobj++)
if (!*ppobj) goto found_handle;

write_unlock(&process->wp_lock);
objput(obj);
return ERR_PTR(-EMFILE);

found_handle:
/* make link to object */
objget(obj);
*ppobj = obj;
write_unlock(&process->wp_lock);
ppobj++; /* don't use the NULL handle */
*hObject = (HANDLE) ((char*)ppobj - (char*)process->wp_handles);
return obj;
} /* end CreateObject() */

这个函数的操作可以分成两大部分。第一部分是对_AllocObject()的调用,旨在创建具体的对象。第二部分是将指向所创建对象的指针“安装”在当前进程的“打开对象表”中,并将相应的下标转换成Handle。为便于阅读讨论,我们先假定第一部分的操作业已完成,_AllocObject()已经返回所创建的Object结构的指针,先看看对于“打开对象表”的操作,这是从注释行“/* find a handle slot */”开始的。至于putname()、objget ()、objput()一类的函数,那只是递增或递减数据结构中的引用计数(减到0就要释放其占用的存储空间),并不影响对于实质性操作的讨论。
“打开对象表”在WineProcess数据结构中,而从上面传下来的只是个WineThread指针。所以这里要通过GetWineProcess()找到当前线程所属的Wine进程,这其实只是从WineThread数据结构中获取其wt_process指针而已。
找到了所属进程的WineProcess数据结构以后,就通过一个for循环扫描其“打开对象表”,旨在找到一个空闲的位置,指针为0就表示空闲。这也说明了为什么0不能被用作handle的值。找到以后,就把新创建对象的obj指针填写到这个位置上。而handle数值的计算,则可以看出基本上是该指针在数组中的(字节)位移量加4,实际上就是下标加1后再乘4。注意handle的值是通过调用参数hObject返回的。
再回到第一部分,即对象的创建,这是由_AllocObject()完成的。

[CreateSemaphoreA() > CreateObject() > _AllocObject()]

static Object *_AllocObject(struct ObjectClass *clss, struct oname *name, void *data)
{
Object *obj;

. . . . . .
/* create and initialise an object */
obj = (Object *) kmalloc(sizeof(Object),GFP_KERNEL);
. . . . . .
atomic_set(&obj->o_count,1);
init_waitqueue_head(&obj->o_wait);
. . . . . .
/* name anonymous objects as "class:objaddr" if so requested */
if (!name->name && ~clss->oc_flags&OCF_DONT_NAME_ANON) {
. . . . . .
}
/* cut'n'paste the name from the caller's name buffer */
else {
obj->o_name.name = name->name;
obj->o_name.nhash = name->nhash;
name->name = NULL;
}

/* attach to appropriate object class list */
obj->o_class = clss;
if (obj->o_name.name)
list_add(&obj->o_objlist,
&clss->oc_nobjs[obj->o_name.nhash&OBJCLASSNOBJSMASK]);
else
list_add(&obj->o_objlist,&clss->oc_aobjs);
. . . . . .
err = clss->constructor(obj,data); /* call the object constructor */
if (err==0) goto cleanup_1;

. . . . . .
cleanup_1:
write_unlock(&clss->oc_lock);
cleanup_0:
return obj;
} /* end _AllocObject() */

首先是由kmalloc()为所创建的对象分配存储空间。然后是Object结构的初始化,包括把对象名(及其hash值)拷贝到Object结构中的o_name里面。
接着,如果有对象名,就根据其hash值把所创建的Object结构挂入所属对象类别的相应hash队列中,否则就挂入该类别的无名对象队列中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: