什么是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
了。使用passwd
给root
加个密码。用exit
退出去然后再用demo
登录,然后再用passwd
改个密码[7]。用vim
或者个人喜欢的编辑器打开/etc/ssh/ssh_config
把PasswordAuthentication 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 right
,port name
是在用户空间中看到的,而port right
则是由Mach管理,就像文件描述符(file descriptor)一样。每个port right
又代表了port
和right
。port
是一个单向沟通通道(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
则拥有接收权利。读者可能会想问为什么要把client
和server
放在同一个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_NONE
和MACH_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_bits
填MACH_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.