你可能没有听说过 Redox OS,这是一款全部基于 Rust 实现的操作系统。基于 Rust 实现的原因之一是,Rust 用于无与伦比的内存管理机制和线程安全机制。为进一步保证系统安全,其采用了一个更加精简的系统内核,并抵制使用复杂庞大的内核。进一步说,在 UNIX 中有一些类似于驱动的东西需要在内核空间中运行,但是在 Redox OS 中,驱动程序甚至可以在用户空间中运行,而且不会导致计算机系统的宕机。

Redox OS 有一套自己的规则来实现进程与内核之间的通信。这套规则叫做 everything be a URL (一切皆 URL)。这套规则朴素但好用。你可以像这样打开一个带路径的文件 scheme:path 。如果没有文件规则限制,则默认为当前工作目录下,通常表示为 file:

什么是一切皆 URL

一切皆 URL 是 Redox 设计规范中一个非常重要的原则。大致意思是,所有 API,设计思路等一整套生态系统都是以 URL 为中心展开,资源左右主要的通信原语。应用与应用直接,与系统之间,与守护进程间等等通信均使用 URL 。举例来说,应用程序不必要创建它们自己专门用来通信的实体。

你可以通用这个规范设计出高一致性、简洁、灵活的接口。

这个规范的作用不会被过度宣扬(这有点超出设计时的初衷了)。这个思路也不是全新创造出来的,Plan9 中就有类似的概念。另外一个成功的概念是 UNIX 中的一切皆文件。

与 ‘一切皆文件’ 的区别

在一切皆文件的概念中,所有类型的设备、进程、内核参数等等都可以在常规的文件系统的中被展现为一个文件。这可能会引发出一些荒诞的事情,例如硬盘中包含了 root 目录的文件系统,根目录 / 中包含了一个叫做 dev 的文件夹,其中包括了含有 root 文件系统的 sda 在内的众多设备文件…… 像这样的情况似乎缺乏了一点逻辑。此外,许多文件属性对许多特殊意义的文件并没有意义,比方说: /dev/null 的大小是多少,或者 sysfs 的配置选项是什么?
相比于一切皆文件的概念,Redox 不会为所有类型的资源来强制实现一个公共可访问的节点树,取而代之的是通过协议来区分的资源。这个方法下 USB 设备将不会展现为文件系统,而是展现为基于协议的一个 scheme ,比如说叫做 EHCI scheme 。真正地文件将会通过一个叫做 file 的 scheme 来访问。更多内容可以了解 RFC 1630RFC 1738.



一些例子

TCP

这里有一个 TCP 协议结构的例子。

1
2
3
4
let mut file = File::open("tcp:93.184.216.34:80")?; //example.com
file.write(b"GET / HTTP/1.1\nHost: example.com\nConnection: close\n\n")?;

io::copy(&mut file, &mut io::stdout())?;

即便网络支持在别的系统中不太容易实现,但是 redox 中却十分简单。你可以像这样开箱即用,open("tcp:address", O_RDWR)
上面这段代码实现了简单的 curls 功能,而且没有依赖任何类库(不像传统意义上的解析 http 协议,更像是一个直接执行 http 请求的库。)


Program IPC

以下是一个 IPC 的例子:

1
2
3
4
5
6
7
8
let server = File::open("chan:hello", O_CREAT)?;

loop {
let stream = syscall::dup(server.as_raw_fd(), b"listen")?;
let mut stream = unsafe {File::from_raw_fd(stream)};

stream.write(b"Hello World!\n")?;
}

你可以调用 open("chan:name", O_CREAT) 来创建一个服务,然后使用 dup("listen") 来通过连接请求。dup 关键词的含义是赋值文件,但是可以选择采用 scheme 中已经使用过的路径。


Events

这个方案有点特殊,不是因为它有任何特殊的硬编码异常,而是因为它在任何地方都使用,即使在其他方案中也是如此。
事件方案有点类似于 linux 的 epoll 。你可以将写入一个事件到一个实例中,这个实例将会在文件中监听和注册。然后你可以读这个事件,直到事件遇到第一次阻塞的时间为止。
下面有一个 event overhaul RFC 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const TOKEN_TIME: usize = 0;
const TOKEN_FILE: usize = 1;

let mut selector = File::open("event:")?;
let mut some_file = /* anything you want to read from here, like a tcp stream */;

// Deadline
let mut timeout = File::open("time:")?;
let mut time = TimeSpec::default();
timeout.read(&mut time)?;
time.tv_sec += 2; // deadline to 2 seconds in the future
timeout.write(&time)?;

selector.write(Event {
id: timeout.as_raw_fd(),
flags: EVENT_READ,
data: TOKEN_TIME
})?;
selector.write(Event {
id: some_file,
flags: EVENT_READ,
data: TOKEN_FILE
})?;

let mut event = Event::default();
selector.read(&mut event)?;

match event.data {
TOKEN_TIME => {
// The timeout of 2 seconds was exceeded
},
TOKEN_FILE => {
// The file can now be read!
// How much can be read is undefined and depends on the scheme.
// Most built-in schemes are nowadays edge triggered and you should
// have the file be non-blocking and read over and over again
// until you hit EAGAIN.
// This matches the behavior of linux.
let mut buf = [0; 16];
loop {
match some_file.read(&mut buf)? {
Ok(0) => break, // EOF, we read everything!
Ok(n) => {
// Handle the read here.
io::stdout().write(&buf[..n])?;
},
Err(ref e) if e.kind() == ErrorKind::WouldBlock => break, // can't read any more this round.
Err(e) => return Err(e)
}
}
}
}