在GNU Hurd中感受Mach微内核的进程通信(IPC)

什么是GNU Hurd

具体的时间线已经在官方维基页面得到详细描述[0],笔者在此就简单叙述一下。在1983年Richard Stallman开启了GNU项目,目的是创建一个自由的操作系统[1]。在接下来的开发中各种软件都已经到位了,其中包括编译器GNU Compiler Collection,编辑器Emacs,C库GNU C Library,调试器GNU Debugger,完整的列表可以查看这个链接。我相信绝大部分的基于Linux的distribution都或多或少的有安装这些软件,虽然可能注意到的人并不多。而其中最重要的问题就是这些应用应该在什么上面运行,答案就是GNU Hurd。正如官方维基说的那样[0],在1983宣布项目开始后,GNU准备使用的内核在很长时间内都没有确定下来,之后在1991年才有了个比较详细的计划:决定使用CMU开发的Mach微内核并且在此之上编写Hurd系统[0]。

一点题外话:在当时其实被看好会做到如今Linux系统规模的主要有两个系统,一个是FreeBSD而另一个就是GNU Hurd。但FreeBSD有一段时间陷入了法律纠纷而GNU Hurd则是开发方面比较缓慢。上一句的信息来源笔者没记错的话是来自《UNIX 编程艺术》。Linus也表示过"如果在1991春天GNU内核已经能在生产中使用了,他是不会启动Linux项目的:但事实是没有。"[3]

微内核(microkernel)

微内核简单来说就是把大部分功能放在用户空间(userspace)。拿Linux举例子就是这个链接指向RTC(Real-Time Clock)的一个驱动代码,而对于GNU Hurd而言,Mach才是它的内核,而跟Linux的RTC驱动代码类似功能的代码[2]并不属于Mach,即并不在内核空间中而是在用户空间内。可以看出好处之一即是如果RTC驱动有Bug会导致崩溃,在Hurd中只有运行着那段RTC驱动代码的进程会崩溃,而Linux则会直接整个崩溃。这也是普遍认为的微内核的好处之一。微内核的好处因为笔者认为已经听得耳朵要起茧了,所以在这里就不多说太多了,接下来就说说坏处。笔者最常听到的是由于IPC的频繁使用而导致频繁发生上下文切换(context switch)而带来的性能下降问题。这个问题在这篇论文中有提及,虽然论文中使用的是L4这个第二代微内核而不是Mach这个第一代微内核成员来和单内核比较,不过L4在进程通信(IPC)方面做了十分强大的优化,所以笔者认为会比Mach更能体现单内核和微内核之间的性能差距。实验使用了基于L4的Linux即L4Linux与纯Linux进行性能比较,结论是微内核给应用带来的性能下降大概在5%到10%。

一点题外话:因为笔者对微内核的了解不深,但有找到这一篇很不错的文章,讲述了三代微内核的特点。推荐给感兴趣的读者。

多服务器(multi-servers)和进程通信(IPC)

正如Hurd主界面介绍的那样,Hurd是一系列在Mach上运行的servers(服务器),并且由这些servers来实现文件系统,网络协议,等等Unix内核或类似内核所拥有的功能。拿上一段提到的RTC驱动来举例子,这个RTC驱动就是像服务器一样由进程在运行,有message(信息)发送到RTC驱动服务器进程后,RTC服务器就会根据信息要求来读取或写入RTC硬件,如果是读取就会把读到的东西再发回去。而传递信息(即Inter-Process Communication)的重任则是由微内核Mach来完成。

Mach Interface Generator (Mach接口生成器)

Mach有着自己的接口来让别的程序使用IPC功能,因为Mach的接口需要尽量考虑到大部分情况,因此其实直接使用是相对复杂的。为了缓解这个问题,一般开发会使用Mach Interface Generator (Mach接口生成器)[5]。它能隐藏掉构造message(信息)的复杂。不过写这篇文章的起因是因为下面这个链接:

http://walfield.org/pub/people/neal/papers/hurd-misc/mach-ipc-without-mig.txt

这个练习的目标是在不使用MIG的前提下写一个利用Mach来完成一次信息发送再得到返回的程序,是用来给人加深对Mach IPC的印象的。

对于想自己捣鼓的读者,笔者就将对练习有用的链接放在这里,GNU Hurd官网的搜索栏十分有用,请好好利用:
https://darnassus.sceen.net/~hurd-web/
https://darnassus.sceen.net/~hurd-web/microkernel/mach/documentation/
http://www.cs.cmu.edu/afs/cs/project/mach/public/doc/osf/kernel_principles.ps
http://www.cs.cmu.edu/afs/cs/project/mach/public/doc/osf/kernel_interface.ps
http://www.cs.cmu.edu/afs/cs/project/mach/public/doc/osf/server_writer.ps

在虚拟机中运行GNU Hurd

GNU Hurd是可以在真实硬件上运行的,但因为设备驱动的不足所以目前GNU Hurd只能在几款比较老款的Thinkpad笔记本上运行[6]。只为体验或完成这份练习的话虚拟机是完全足够的了。

目前相对比较稳定的Distribution是Debian。64位系统还不是非常完善,所以我们先使用32位的。可以在这个链接中查看所有可用Images。我们能运行以下指令来获得32位的Debian GNU Hurd Image。

复制代码
wget https://cdimage.debian.org/cdimage/ports/latest/hurd-i386/current/debian-hurd.img.gz

然后把文件解压:

复制代码
gzip -d debian-hurd.img.gz

笔者使用的是qemu,所以用这个指令来运行:

复制代码
qemu-system-i386 -enable-kvm -m 2G -drive \
    format=raw,cache=writeback,file=debian-hurd.img \
    -nic user,hostfwd=tcp::2222-:22 -display curses -vga std

启动完成后会看到

复制代码
Debian GNU/Hurd 12 debian tty3

login:

直接输入root然后Enter就能进入bash了。使用passwdroot加个密码。用exit退出去然后再用demo登录,然后再用passwd改个密码[7]。用vim或者个人喜欢的编辑器打开/etc/ssh/ssh_configPasswordAuthentication yes那行的#去掉。接下来我们就可以直接再开一个新的terminal然后用ssh来在GNU Hurd上操作了:

复制代码
ssh -p 2222 demo@localhost

然后准备工作就都完成了,接下来就可以开始研究练习了。

Mach IPC without MIG

练习目标是要在一个进程中创建两个线程,一个是客户一个是伺服器。客户要给伺服器发送一个信息并且要收到一个伺服器的回应。而信息的传递要使用Mach微内核的接口。

两个线程

我们就先写出两个线程吧。

复制代码
#include <threads.h>
#include <stdio.h>

int
server (void *arg)
{
    printf ("I'm server\n");
}

int
client (void *arg)
{
    printf ("I'm client\n");
}

int
main (void)
{
    thrd_t server_thrd, client_thrd;

    thrd_create (&server_thrd, server, NULL);
    thrd_create (&client_thrd, client, NULL);
    thrd_join (server_thrd, NULL);
    thrd_join (client_thrd, NULL);
    return 0;
}

编译时要用这条指令:

复制代码
gcc main.c -lpthread -o main

Ports

我们所编译出来的的main可执行文件在刚开始会由一个线程(thread)运行,在运行到两个thrd_create时又会多生出两个线程,那么总共就会有三个线程运行我们的代码,当然运行的路线(routine)是不同的,一个是main(),一个是server()而剩下的那个则是client()。正如Mach 3 Kernel Principles[9]中所描述的那样,这三个线程都是属于同一个task的。每个task会有一个port name space,每个port name代表一个port rightport name是在用户空间中看到的,而port right则是由Mach管理,就像文件描述符(file descriptor)一样。每个port right又代表了portrightport是一个单向沟通通道(unidirectional communication channel),right则代表了task能对跟right一起的port所进行的操作。right的类型包括接收权限(receive right),发送权限(send right),单次发送权限(send-once right)等等[10]。简单来说就是如果想要用Mach沟通,那就先要有一个port。我们可以使用mach_port_allocate来获得一个port

git diff

复制代码
diff --git a/main.c b/main.c
index f73724a..2eb9f38 100644
--- a/main.c
+++ b/main.c
@@ -1,5 +1,8 @@
 #include <threads.h>
 #include <stdio.h>
+#include <mach.h>
+
+static mach_port_t port;

 int
 server (void *arg)
@@ -17,6 +20,7 @@ int
 main (void)
 {
     thrd_t server_thrd, client_thrd;
+    mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE, &port);

     thrd_create (&server_thrd, server, NULL);
     thrd_create (&client_thrd, client, NULL);

这样子我们就获得了一个在自己的task中的port,而我们对这个port则拥有接收权利。读者可能会想问为什么要把clientserver放在同一个task中而不是分开两个放。正如练习中描述的那样[8],放在同一个task中是为了简化实现,不然就会涉及到另一个关于central name server的课题了。

Message

练习描述中有提到信息的结构[8]:

复制代码
Inline messages have the following general structure:

 [ [mach_msg_header_t] [[mach_msg_type_t][data]] [[mach_msg_type_t][data]]... ]
 ^ ^                                      ^^                               ^             ^
 | |                                       ||                              |                \- Second set of arguments
 | |                                       ||                              \- First set of arguments
 | \- Message header        |\- Header for first set of arguments
 \- Message                         \- Pay load

When data is marked out of line, the data section is detached.

我们打算让Client发送:"Alice\0"给Server并且让Server回复"Hello Alice\0",我们可以创建一个这样的结构:
git diff

复制代码
diff --git a/main.c b/main.c
index 2eb9f38..2307de1 100644
--- a/main.c
+++ b/main.c
@@ -2,6 +2,13 @@
 #include <stdio.h>
 #include <mach.h>

+struct message
+{
+    mach_msg_header_t msg_header;
+    mach_msg_type_t first_header;
+    char string[100];
+};
+
 static mach_port_t port;

 int
@@ -13,6 +20,7 @@ server (void *arg)
 int
 client (void *arg)
 {
+    struct message m;
     printf ("I'm client\n");
 }

Client

在Mach kernel interface中能看到mach_msg就将是我们用来发送和接收message的接口函数了。头文件声明则在这里

复制代码
extern mach_msg_return_t
mach_msg
   (mach_msg_header_t *msg,
    mach_msg_option_t option,
    mach_msg_size_t send_size,
    mach_msg_size_t rcv_size,
    mach_port_name_t rcv_name,
    mach_msg_timeout_t timeout,
    mach_port_name_t notify);

可见参数还是蛮多的,参数在Mach 3 Kernel Interface中有介绍,笔者在此斗胆翻译一下:

msg: A message buffer.

即该变量指向存放着信息消息头的区域。在我们目前的代码中其实就是&(m.msg_header)。(笔者认为这里就很像网络协议中的消息头)

option: Message options are bit values, combined with bitwise-or. One or both of MACH_SEND_MSG and MACH_RCV_MSG should be used.

正如上面所言mach_msg既能发送也能用来接收,可能会有读者好奇 MACH_SEND_MSG | MACH_RCV_MSG代表了什么。它意味的发送了信息后还会期待一个信息返回回来,而它正是我们所要在Client里使用的了。

send_size: When sending a message, specifies the size of the message buffer. Otherwise zero should be supplied.

请注意,这里提到的message并不是"Alice\0"Hello Alice\0,而是在上一节所提到的message。所以就是sizeof (struct message)

rcv_size: When receiving a message, specifies the size of the message buffer. Otherwise zero should be supplied.

因为我们也打算收到一个回应,所以我们要填的值就是sizeof (struct message)

rcv_name: When receiving a message, specifies the port or port set. Otherwise MACH_PORT_NULL should be supplied.

我们会从变量port接收信息,所以就填它。

timeout 和 notify

我们在这次练习中不怎么在乎所以就填MACH_MSG_TIMEOUT_NONEMACH_PORT_NULL

所以我们在Client中使用的mach_msg就会如下所示:

复制代码
mach_msg (&(m.msg_header), MACH_SEND_MSG | MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

当然在这之前还要加上变量m的一些初始化。

先是记录在Mach 3 Kernel Interface 327页的mach_msg_header

复制代码
typedef struct mach_msg_header {
    mach_msg_bits_t       msgh_bits;
    mach_msg_size_t      msgh_size;
    mach_port_t               msgh_remote_port;
    mach_port_t               msgh_local_port;
    mach_port_seqno_t  msgh_seqno;
    mach_msg_id_t          msgh_id;
} mach_msg_header_t;

可见参数也不少。

msgh_bits

还记得我们在main中分配的那个right吗,那是个发送权限,而正如练习中提到的:

Mach ports: how to allocate send and receive rights (the former implicitly in mach_msg and the latter directly via mach_port_allocate).

我们将会在msgh_bits中分配发送权限。我们会给msgh_bitsMACH_MSGH_BITS (MACH_MSG_TYPE_MAKE_SEND, 0)。这个操作意味着放一个之后变量msgh_remote_port所提及的port的发送权限到message中。没错在Mach中权限是可以通过IPC传来传去的,这也是capability-based的体现。因为在前面mach_msg中填的port的接收方还是我们这个task,因此我们可以获得一个发送权限。当然分享权限是有前提的,在源码中能看到MACH_MSG_TYPE_MAKE_SEND的前提是持有发送权限,而我们则已经在main中获取了发送权限[12]。

msgh_size

填入sizeof (struct message)就行。

msgh_remote_port: When sending, specifies the destination port of the message. The field must carry a legitimate send or send-once right for a port. When received, this field is swapped with msgh_local_port.

在这个练习中就是变量port。因为我们在msgh_bits中的操作,我们会有一个发送权限。

msgh_local_port: When sending, specifies an auxiliary port right, which is conventionally used as a reply port by the recipient of the message. ......

接收port我们就填个MACH_PORT_NULL

msgh_seqno: The sequence number of this message relative to the port from which it is received. This field is ignored on sent messages.

因为就一个message,就填入0。

msgh_id: Not set or read by the mach_msg call. ......

我们用不到所以不管。

因此结果会是如下:

复制代码
m.msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
m.msg_header.msgh_size = sizeof (struct message);
m.msg_header.msgh_remote_port = port;
m.msg_header.msgh_local_port = MACH_PORT_NULL;
m.msg_header.msgh_seqno = 0;

再接下来就是m.first_header,描述在Mach 3 Kernel Interface 330页。

复制代码
typedef struct {
    unsigned int        msgt_name : 8,
                                   msgt_size : 8,
                                   msgt_number : 12,
                                   msgt_inline : 1,
                                   msgt_longform : 1,
                                   msgt_deallocate : 1,
                                   msgt_unused : 1;
} mach_msg_type_t

msgt_name

我们要传输的数据是"Alice\0""Hello Alice\0",所以是MACH_MSG_TYPE_STRING_C

msgt_size: Specifies the size of each datum, in bits.

一个char是一字节也就是8 bits,所以是8.

msgt_number: Specifies how many data elements comprise the data item.

我们就选择100。

msgt_inline: When FALSE, specifies that the data actucally resides in an out-of-line region. ......

我们的信息比较短能直接存在message里所以是true

msgt_longform: Specifies, when TRUE, that this type descriptor is a mach_msg_type_long_t instead of a mach_msg_type_t.

我们是mach_msg_type_t所以是false

msgt_unused: Not used, should be zero.

那就是0。

所以结果如下:

复制代码
m.first_header.msgt_name = MACH_MSG_TYPE_STRING_C;
m.first_header.msgt_size = 8;
m.first_header.msgt_number = 100;
m.first_header.msgt_inline = true;
m.first_header.msgt_longform = false;
m.first_header.msgt_unused = 0;

最后再整合一下:
git diff

复制代码
diff --git a/main.c b/main.c
index 2307de1..fcea63d 100644
--- a/main.c
+++ b/main.c
@@ -1,6 +1,8 @@
 #include <threads.h>
 #include <stdio.h>
 #include <mach.h>
+#include <string.h>
+#include <stdbool.h>
 
 struct message
 {
@@ -21,7 +23,28 @@ int
 client (void *arg)
 {
     struct message m;
-    printf ("I'm client\n");
+    mach_msg_return_t err;
+
+    m.msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
+    m.msg_header.msgh_size = sizeof (struct message);
+    m.msg_header.msgh_remote_port = port;
+    m.msg_header.msgh_local_port = MACH_PORT_NULL;
+    m.msg_header.msgh_seqno = 0;
+
+    m.first_header.msgt_name = MACH_MSG_TYPE_STRING_C;
+    m.first_header.msgt_size = 8;
+    m.first_header.msgt_number = 100;
+    m.first_header.msgt_inline = true;
+    m.first_header.msgt_longform = false;
+    m.first_header.msgt_unused = 0;
+
+    strcpy (m.string, "Alice\0");
+
+    err = mach_msg (&(m.msg_header), MACH_SEND_MSG | MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
+
+    printf ("Client error code: %d\n", err);
+
+    printf ("From Server: %s\n", m.string);
 }
 
 int

笔者增加了一个err变量来检查mach_msg是否成功,现在编译后运行的话应该会看到如下的输出:

复制代码
I'm server
Client error code: 0
From Server: Alice

err的值是0说明成功了,当然这不是我们最终想要的,我们还要把Server给写了,出现这个输出的原因笔者认为是Client接收了自己发出去的message所以mach_msg成功并且打印了"Alice\0"

Server

比较艰难的阶段已经过去了,接下来的操作都跟之前写Client时有关了。Server要先接收信息,所以只要MACH_RCV_MSG而不是MACH_SEND_MSG | MACH_RCV_MSG

git diff:

复制代码
diff --git a/main.c b/main.c
index fcea63d..8200286 100644
--- a/main.c
+++ b/main.c
@@ -16,7 +16,13 @@ static mach_port_t port;
 int
 server (void *arg)
 {
-    printf ("I'm server\n");
+    struct message m;
+    mach_msg_return_t err;
+
+    err = mach_msg (&(m.msg_header), MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
+
+    printf ("Server receive error code: %d\n", err);
+    printf ("Server: from client: %s\n", m.string);
 }

 int

现在再编译运行的话我们就会看到:

复制代码
Server receive error code: 0
Server: from client: Alice
<卡在这>

先庆祝一下!这意味着Server成功从Client接收了一个信息,而卡着说明Client正在等待着来自Server的回复。当然Client这辈子都等不到它的回复了,因为我们还没写。那就Ctrl-C结束进程然后把最后一步给写上去吧。

其实剩下的只要复制Client的发送方式就行啦:
git diff

复制代码
diff --git a/main.c b/main.c
index 8200286..03ed4e2 100644
--- a/main.c
+++ b/main.c
@@ -18,11 +18,32 @@ server (void *arg)
 {
     struct message m;
     mach_msg_return_t err;
+    char s[100];
 
     err = mach_msg (&(m.msg_header), MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
 
     printf ("Server receive error code: %d\n", err);
     printf ("Server: from client: %s\n", m.string);
+
+    m.msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
+    m.msg_header.msgh_size = sizeof (struct message);
+    m.msg_header.msgh_remote_port = port;
+    m.msg_header.msgh_local_port = MACH_PORT_NULL;
+    m.msg_header.msgh_seqno = 0;
+
+    m.first_header.msgt_name = MACH_MSG_TYPE_STRING_C;
+    m.first_header.msgt_size = 8;
+    m.first_header.msgt_number = 100;
+    m.first_header.msgt_inline = true;
+    m.first_header.msgt_longform = false;
+    m.first_header.msgt_unused = 0;
+
+    sprintf (s, "Hello %s\n\0", m.string);
+    strcpy (m.string, s);
+
+    err = mach_msg (&(m.msg_header), MACH_SEND_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
+
+    printf ("Server send error code: %d\n", err);
 }
 
 int

最后编译运行后的输出:

复制代码
Server receive error code: 0
Server: from client: Alice
Server send error code: 0
Server send error code: 0
Client error code: 0
From Server: Hello Alice

Client成功接收到了Server的回复!成功了!笔者就是不太清楚为什么会出现两次Server send error code: 0。不过至少是成功了。

最后再放个完整的代码:

复制代码
#include <threads.h>
#include <stdio.h>
#include <mach.h>
#include <string.h>
#include <stdbool.h>

struct message
{
    mach_msg_header_t msg_header;
    mach_msg_type_t first_header;
    char string[100];
};

static mach_port_t port;

int
server (void *arg)
{
    struct message m;
    mach_msg_return_t err;
    char s[100];

    err = mach_msg (&(m.msg_header), MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf ("Server receive error code: %d\n", err);
    printf ("Server: from client: %s\n", m.string);

    m.msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    m.msg_header.msgh_size = sizeof (struct message);
    m.msg_header.msgh_remote_port = port;
    m.msg_header.msgh_local_port = MACH_PORT_NULL;
    m.msg_header.msgh_seqno = 0;

    m.first_header.msgt_name = MACH_MSG_TYPE_STRING_C;
    m.first_header.msgt_size = 8;
    m.first_header.msgt_number = 100;
    m.first_header.msgt_inline = true;
    m.first_header.msgt_longform = false;
    m.first_header.msgt_unused = 0;

    sprintf (s, "Hello %s\n\0", m.string);
    strcpy (m.string, s);

    err = mach_msg (&(m.msg_header), MACH_SEND_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf ("Server send error code: %d\n", err);
}

int
client (void *arg)
{
    struct message m;
    mach_msg_return_t err;

    m.msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    m.msg_header.msgh_size = sizeof (struct message);
    m.msg_header.msgh_remote_port = port;
    m.msg_header.msgh_local_port = MACH_PORT_NULL;
    m.msg_header.msgh_seqno = 0;

    m.first_header.msgt_name = MACH_MSG_TYPE_STRING_C;
    m.first_header.msgt_size = 8;
    m.first_header.msgt_number = 100;
    m.first_header.msgt_inline = true;
    m.first_header.msgt_longform = false;
    m.first_header.msgt_unused = 0;

    strcpy (m.string, "Alice\0");

    err = mach_msg (&(m.msg_header), MACH_SEND_MSG | MACH_RCV_MSG, sizeof (struct message), sizeof (struct message), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf ("Client error code: %d\n", err);

    printf ("From Server: %s\n", m.string);
}

int
main (void)
{
    thrd_t server_thrd, client_thrd;
    mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE, &port);

    thrd_create (&server_thrd, server, NULL);
    thrd_create (&client_thrd, client, NULL);
    thrd_join (server_thrd, NULL);
    thrd_join (client_thrd, NULL);
    return 0;
}

总结

可以看到MIG还是帮开发者减少了很多烦恼的。在练习中也能更加清晰的感受到微内核的IPC是怎么一回事。在这里也感谢Hurd的开发者们和练习的作者Neal H Walfield的付出。有任何不正确的地方欢迎指出。


作者:chenw1

链接:https://www.cnblogs.com/chenw1/p/18768256

本文来自博客园,欢迎转载,但请注明原文链接,并保留此段声明,否则保留追究法律责任的权利。

All right reserved.