Merge pull request #11 from andream7/master

Update ch01-02/03.md
master
chyyuu 4 years ago committed by GitHub
commit 28277c1466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,108 +1,312 @@
#### 对象管理器Process 对象
## 权限
## 句柄——操作内核对象的桥梁
[权限]: https://github.com/zhangpf/fuchsia-docs-zh-CN/blob/master/zircon/docs/rights.md
在1.1中我们用Rust语言实现了一个最核心的内核对象在本小节我们将逐步了解与内核对象相关的三个重要概念中的其他两个**句柄Handle和权限Rights**。
内核对象的“[权限](https://fuchsia.dev/docs/concepts/kernel/rights)”指定允许对内核对象进行哪些操作。权限与句柄相关联,并传达对关联句柄或与句柄关联的对象执行操作的特权。单个进程可能对具有不同权限的同一个内核对象有两个不同的句柄
句柄是允许用户程序引用内核对象引用的一种内核结构,它可以被认为是与特定内核对象的会话或连接
## 句柄
通常情况下,多个进程通过不同的句柄同时访问同一个对象。对象可能有多个句柄(在一个或多个进程中)引用它们。但单个句柄只能绑定到单个进程或绑定到内核。
[句柄]: https://github.com/zhangpf/fuchsia-docs-zh-CN/blob/master/zircon/docs/handles.md
### 定义句柄
在 object 模块下定义一个子模块:
句柄是允许用户程序引用内核对象引用的一种内核结构,它可以被认为是与特定内核对象的会话或连接。
```rust,noplaypen
// src/object/mod.rs
mod handle;
通常情况下,多个进程通过不同的句柄同时访问同一个对象。对象可能有多个句柄(在一个或多个进程中)引用它们。但单个句柄只能绑定到单个进程或绑定到内核。
pub use self::handle::*;
```
当句柄绑定到内核时,我们说它是“在传输中”('in-transit')。
定义句柄:
在用户模式下,句柄只是某个系统调用返回的特定数字。只有“不在传输中”的句柄对用户模式可见。
```rust,noplaypen
// src/object/handle.rs
use super::{KernelObject, Rights};
use alloc::sync::Arc;
/// 内核对象句柄
#[derive(Clone)]
pub struct Handle {
pub object: Arc<dyn KernelObject>,
pub rights: Rights,
}
```
代表句柄的整数只对其所属的那个进程有意义。另一个进程中的相同数字可能不会映射到任何句柄,或者它可能映射到指向完全不同的内核对象的句柄。
一个Handle包含object和right两个字段object是实现了`KernelObject`Trait的内核对象Rights是该句柄的权限我们将在下面提到它
句柄的整数值是任何 32 位数字,但对应于**ZX_HANDLE_INVALID**的值将始终为 0。除此之外有效句柄的整数值将始终具有句柄集的两个最低有效位. 可以使用**ZX_HANDLE_FIXED_BITS_MASK**访问代表这些位的掩码。
Arc<T>是一个可以在多线程上使用的引用计数类型,这个计数会随着 `Arc<T>` 的创建或复制而增加,并当 `Arc<T>` 生命周期结束被回收时减少。当这个计数变为零之后,这个计数变量本身以及被引用的变量都会从堆上被回收
句柄可以从一个进程移动到另一个进程,方法是将它们写入通道(使用[`channel_write()`](https://fuchsia.dev/docs/reference/syscalls/channel_write)),或者使用 [`process_start()`](https://fuchsia.dev/docs/reference/syscalls/process_start)传递一个句柄作为新进程中第一个线程的参数。对于几乎所有的对象,当最后一个打开的引用对象的句柄关闭时,对象要么被销毁,要么被置于可能无法撤消的最终状态。
我们为什么要在这里使用Arc智能指针呢
绝大多数内核对象的析构都发生在句柄数量为 0 时也就是最后一个指向内核对象的Handle被关闭该对象也随之消亡抑或进入一种无法撤销的最终状态。很明显这与Arc<T>天然的契合。
## 控制句柄的权限——Rights
`Cargo.toml` 中加入 `bitflags` 库:
上文的Handle中有一个字段是rights也就是句柄的权限。顾名思义权限规定该句柄对引用的对象可以进行何种操作。
```rust,noplaypen
[dependencies]
{{#include ../../code/ch01-02/Cargo.toml:bitflags}}
```
当不同的权限和同一个对象绑定在一起时,也就形成了不同的句柄。
在 object 模块下定义两个子模块:
### 定义权限
在 object 模块下定义一个子模块:
```rust,noplaypen
// src/object/mod.rs
{{#include ../../code/ch01-02/src/object/mod.rs:mod}}
mod rights;
pub use self::rights::*;
```
定义权限:
权限就是u32的一个数字
```rust,noplaypen
// src/object/rights.rs
{{#include ../../code/ch01-02/src/object/rights.rs:rights}}
use bitflags::bitflags;
bitflags! {
/// 句柄权限
pub struct Rights: u32 {
const DUPLICATE = 1 << 0;
const TRANSFER = 1 << 1;
const READ = 1 << 2;
const WRITE = 1 << 3;
const EXECUTE = 1 << 4;
...
}
```
定义句柄:
[**bitflags**](https://docs.rs/bitflags/1.2.1/bitflags/) 是一个 Rust 中常用来比特标志位的 crate 。它提供了 一个 `bitflags!` 宏,如上面的代码段所展示的那样,借助 `bitflags!` 宏我们将一个 `u32` 的 rights 包装为一个 `Rights` 结构体。注意,在使用之前我们需要引入该 crate 的依赖
```rust,noplaypen
// src/object/handle.rs
{{#include ../../code/ch01-02/src/object/handle.rs:handle}}
# Cargo.toml
[dependencies]
bitflags = "1.2"
```
## 存储内核对象句柄
定义好权限之后,我们回到句柄相关方法的实现。
> 添加成员变量 handles: BTreeMap<HandleValue, Handle>
>
> 实现 createadd_handleremove_handle 函数
首先是最简单的部分创建一个handle很显然我们需要提供两个参数分别是句柄关联的内核对象和句柄的权限。
使用上一节的方法,实现一个空的 Process 对象:
```rust,noplaypen
impl Handle {
/// 创建一个新句柄
pub fn new(object: Arc<dyn KernelObject>, rights: Rights) -> Self {
Handle { object, rights }
}
}
```
### 测试
好啦,让我们来测试一下!
```rust,noplaypen
// src/task/process.rs
{{#include ../../code/ch01-02/src/task/process.rs:process}}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::DummyObject;
#[test]
fn new_obj_handle() {
let obj = DummyObject::new();
let handle1 = Handle::new(obj.clone(), Rights::BASIC);
}
}
```
插入、删除句柄函数:
## 句柄存储的载体——Process
实现完了句柄之后,我们开始考虑,句柄是存储在哪里的呢?
通过前面的讲解很明显Process拥有内核对象句柄也就是说句柄存储在Process中所以我们先来实现一个Process
### 实现空的process对象
```rust,noplaypen
// src/task/process.rs
impl Process {
{{#include ../../code/ch01-02/src/task/process.rs:add_remove_handle}}
/// 进程对象
pub struct Process {
base: KObjectBase,
inner: Mutex<ProcessInner>,
}
// 宏的作用:补充
impl_kobject!(Process);
struct ProcessInner {
handles: BTreeMap<HandleValue, Handle>,
}
pub type HandleValue = u32;
```
## 定义内核错误及 `Result` 类型
handles使用BTreeMap存储的key是HandleValuevalue就是句柄。通过HandleValue实现对句柄的增删操作。HandleValue实际上就是u32类型是别名。
把内部对象ProcessInner用自旋锁Mutex包起来保证了互斥访问因为Mutex会帮我们处理好并发问题这一点已经在1.1节中详细说明。
接下来我们实现创建一个Process的方法
```rust,noplaypen
// src/error.rs
{{#include ../../code/ch01-02/src/error.rs:error_begin}}
impl Process {
/// 创建一个新的进程对象
pub fn new() -> Arc<Self> {
Arc::new(Process {
base: KObjectBase::default(),
inner: Mutex::new(ProcessInner {
handles: BTreeMap::default(),
}),
})
}
}
```
// ......
#### 单元测试
{{#include ../../code/ch01-02/src/error.rs:error_end}}
我们已经实现了创建一个Process的方法下面我们写一个单元测试
```rust,noplaypen
#[test]
fn new_proc() {
let proc = Process::new();
assert_eq!(proc.type_name(), "Process");
assert_eq!(proc.name(), "");
proc.set_name("proc1");
assert_eq!(proc.name(), "proc1");
assert_eq!(
format!("{:?}", proc),
format!("Process({}, \"proc1\")", proc.id())
);
let obj: Arc<dyn KernelObject> = proc;
assert_eq!(obj.type_name(), "Process");
assert_eq!(obj.name(), "proc1");
obj.set_name("proc2");
assert_eq!(obj.name(), "proc2");
assert_eq!(
format!("{:?}", obj),
format!("Process({}, \"proc2\")", obj.id())
);
}
```
### Process相关方法
#### 插入句柄
在Process中添加一个新的handle返回值是一个handleValue也就是u32
```rust,noplaypen
// src/error.rs
{{#include ../../code/ch01-02/src/error.rs:result}}
pub fn add_handle(&self, handle: Handle) -> HandleValue {
let mut inner = self.inner.lock();
let value = (0 as HandleValue..)
.find(|idx| !inner.handles.contains_key(idx))
.unwrap();
// 插入BTreeMap
inner.handles.insert(value, handle);
value
}
```
## 根据句柄查找内核对象
#### 移除句柄
删除Process中的一个句柄
```rust,noplaypen
pub fn remove_handle(&self, handle_value: HandleValue) {
self.inner.lock().handles.remove(&handle_value);
}
```
> 实现 get_object_with_rights 等其它相关函数
>
> 实现 handle 单元测试
#### 根据句柄查找内核对象
```rust,noplaypen
// src/task/process.rs
impl Process {
{{#include ../../code/ch01-02/src/task/process.rs:get_object_with_rights}}
/// 根据句柄值查找内核对象,并检查权限
pub fn get_object_with_rights<T: KernelObject>(
&self,
handle_value: HandleValue,
desired_rights: Rights,
) -> ZxResult<Arc<T>> {
let handle = self
.inner
.lock()
.handles
.get(&handle_value)
.ok_or(ZxError::BAD_HANDLE)?
.clone();
// check type before rights
let object = handle
.object
.downcast_arc::<T>()
.map_err(|_| ZxError::WRONG_TYPE)?;
if !handle.rights.contains(desired_rights) {
return Err(ZxError::ACCESS_DENIED);
}
Ok(object)
}
}
```
#### ZxResult
ZxResult是表示Zircon状态的i32值值空间划分如下
- 0:ok
- 负值:由系统定义(也就是这个文件)
- 正值:被保留,用于协议特定的错误值,永远不会被系统定义。
```rust,noplaypen
pub type ZxResult<T> = Result<T, ZxError>;
#[allow(non_camel_case_types, dead_code)]
#[repr(i32)]
#[derive(Debug, Clone, Copy)]
pub enum ZxError {
OK = 0,
...
/// 一个不指向handle的特定的handle value
BAD_HANDLE = -11,
/// 操作主体对于执行这个操作来说是错误的类型
/// 例如: 尝试执行 message_read 在 thread handle.
WRONG_TYPE = -12,
// 权限检查错误
// 调用者没有执行该操作的权限
ACCESS_DENIED = -30,
}
```
ZxResult<T>相当于Result<T, ZxError>,也就相当于我们自己定义了一种错误。
### 单元测试
目前为止我们已经实现了Process最基础的方法下面我们来运行一个单元测试
```rust,noplaypen
fn proc_handle() {
let proc = Process::new();
let handle = Handle::new(proc.clone(), Rights::DEFAULT_PROCESS);
let handle_value = proc.add_handle(handle);
let object1: Arc<Process> = proc
.get_object_with_rights(handle_value, Rights::DEFAULT_PROCESS)
.expect("failed to get object");
assert!(Arc::ptr_eq(&object1, &proc));
proc.remove_handle(handle_value);
}
```
## 总结
在这一节中我们实现了内核对象的两个重要的概念句柄Handle和权限Rights同时实现了句柄存储的载体——Process并且实现了Process的基本方法这将是我们继续探索zCore的基础。
在下一节中我们将介绍内核对象的传输器——管道Channel

@ -4,36 +4,239 @@
通道Channel是由一定数量的字节数据和一定数量的句柄组成的双向消息传输。
## 描述
## 用于IPC的内核对象
通道有两个端点endpoints。从逻辑上讲每个端点都维护要读取的有序消息队列。写入一个端点会将消息排入另一个端点的队列中。当端点的最后一个句柄关闭时该端点队列中的未读消息将被销毁。因为销毁消息会关闭消息包含的所有句柄关闭通道端点可能会产生递归效果例如通道包含一条消息它包含一个通道它包含一条消息等等
Zircon中用于IPC的内核对象主要有Channel、Socket和FIFO。这里我们主要介绍一下前两个
关闭通道的最后一个句柄对先前写入该通道的消息的生命周期没有影响。这为通道提供了“即发即忘”的语义
> **进程间通信****IPC***Inter-Process Communication*),指至少两个进程或线程间传送数据或信号的一些技术或方法。进程是计算机系统分配资源的最小单位(进程是分配资源最小的单位,而线程是调度的最小单位,线程共用进程资源)。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。举一个典型的例子,使用进程间通信的两个应用可以被分类为客户端和服务器,客户端进程请求数据,服务端回复客户端的数据请求。有一些应用本身既是服务器又是客户端,这在分布式计算中,时常可以见到。这些进程可以运行在同一计算机上或网络连接的不同计算机上
一条消息由一定数量的数据和一定数量的句柄组成。调用[`channel_write()`](https://fuchsia.dev/docs/reference/syscalls/channel_write)使一条消息入队,调用[`channel_read()`](https://fuchsia.dev/docs/reference/syscalls/channel_read) 使一条消息出列(如果有队列)。线程可以阻塞,直到消息通过[`object_wait_one()`](https://fuchsia.dev/docs/reference/syscalls/object_wait_one)或其他等待机制挂起
`Socket`和`Channel`都是双向和双端的IPC相关的`Object`。创建`Socket`或`Channel`将返回两个不同的`Handle`,分别指向`Socket`或`Channel`的两端。与channel的不同之处在于socket仅能传输数据而不移动句柄而channel可以传递句柄
或者,调用[`channel_call()`](https://fuchsia.dev/docs/reference/syscalls/channel_call)在通道的一个方向上将消息入队等待相应的响应然后将响应消息出队。在调用模式call mode相应的响应通过消息的前 4 个字节标识,称为事务 IDtransaction ID。内核使用[`channel_call()`](https://fuchsia.dev/docs/reference/syscalls/channel_call),为消息提供唯一的事务 ID.
- `Socket`是面向流的对象,可以通过它读取或写入以一个或多个字节为单位的数据。
- `Channel`是面向数据包的对象并限制消息的大小最多为64K如果有改变可能会更小以及最多1024个`Handle`挂载到同一消息上(如果有改变,同样可能会更小)。
通过通道发送消息的过程有两个步骤。第一步是原子地将数据写入通道并将消息中所有句柄的所有权移到此通道中。此操作始终消耗句柄在调用结束时所有句柄要么全部在通道中要么全部丢弃。第二步操作通道读取channel read与第一步类似成功后下一条消息中的所有句柄都被原子地移动到接收进程的句柄表中。失败时通道将保留所有权然后它们将被删除
当`Handle`被写入到`Channel`中时,在发送端`Process`中将会移除这些`Handle`。同时携带`Handle`的消息从`Channel`中被读取时,该`Handle`也将被加入到接收端`Process`中。在这两个时间点之间时,`Handle`将同时存在于两端(以保证它们指向的`Object`继续存在而不被销毁),除非`Channel`写入方向一端被关闭,这种情况下,指向该端点的正在发送的消息将被丢弃,并且它们包含的任何句柄都将被关闭
与许多其他内核对象类型不同通道是不可复制的。因此只有一个句柄与通道端点相关联持有该句柄的进程被视为所有者owner。只有所有者可以读取或写入消息或将通道端点发送到另一个进程。
## Channel
当通道端点的所有权从一个进程转移到另一个进程时,即使消息正在进行写入,消息也不会被重新排序或截断。转移事件之前的消息属于以前的所有者,转移之后的消息属于新的所有者。如果在传输端点时,正在进行消息读取,则之前描述的所有权转移方式同样适用
Channel是唯一一个能传递handle的IPC其他只能传递消息。通道有两个端点`endpoints`,对于代码实现来说,**通道是虚拟的,我们实际上是用通道的两个端点来描述一个通道**。两个端点各自要维护一个消息队列,在一个端点写消息,实际上是把消息写入**另一个端点**的消息队列队尾;在一个端点读消息,实际上是从**当前端点**的消息队列的队头读出一个消息
即使最后剩余的句柄被剥夺了**DUPLICATE**权限,也不为其他内核对象提供上述顺序保证。
消息通常含有`data`和`handles`两部分,我们这里将消息封装为`MessagePacket`结构体,结构体中含有上述两个字段:
## 创建一对内核对象
```rust,noplaypen
#[derive(Default)]
pub struct MessagePacket {
/// message packet携带的数据data
pub data: Vec<u8>,
/// message packet携带的句柄Handle
pub handles: Vec<Handle>,
}
```
## 创建通道
### 实现空的Channel对象
创建 Channel 将返回两个句柄,一个指向对象的每个端点。
在`src`目录下创建一个`ipc`目录,在`ipc`模块下定义一个子模块`channel`
> 实现 Channel::create
>
> 讲一下互相持有对方 Weak 指针的目的,这里有不可避免的 unsafe
```rust,noplaypen
// src/ipc/mod.rs
use super::*;
## 实现数据传输
mod channel;
pub use self::channel::*;
```
当句柄被写入通道时,它们会从发送进程中删除。当从通道读取带有句柄的消息时,句柄被添加到接收进程中。在这两个事件之间,句柄继续存在(确保它们所指的对象继续存在),除非它们写入的通道的末端关闭——此时发送到该端点的消息被丢弃并且它们包含的任何句柄都已关闭。
在`ipc.rs`中引入`crate`
> 实现 read, write 函数read_write 单元测试
```rust,noplaypen
// src/ipc/channel.rs
use {
super::*,
crate::error::*,
crate::object::*,
alloc::collections::VecDeque,
alloc::sync::{Arc, Weak},
spin::Mutex,
};
```
把在上面提到的`MessagePacket`结构体添加到该文件中。
下面我们添加Channel结构体
```rust,noplaypen
// src/ipc/channel.rs
pub struct Channel {
base: KObjectBase,
peer: Weak<Channel>,
recv_queue: Mutex<VecDeque<T>>,
}
type T = MessagePacket;
```
`peer`代表当前端点所在管道的另一个端点,两端的结构体分别持有对方的`Weak`引用,并且两端的结构体将分别通过`Arc`引用作为内核对象而被内核中的其他数据结构引用这一部分我们将在创建Channel实例时提到。
`recv_queue`代表当前端点维护的消息队列,它使用`VecDeque`来存放`MessagePacket`,可以通过`pop_front()`、`push_back`等方法在队头弹出数据和在队尾压入数据。
用使用宏自动实现 `KernelObject` trait 使用channel类型名并添加两个函数。
```rust,noplaypen
impl_kobject!(Channel
fn peer(&self) -> ZxResult<Arc<dyn KernelObject>> {
let peer = self.peer.upgrade().ok_or(ZxError::PEER_CLOSED)?;
Ok(peer)
}
fn related_koid(&self) -> KoID {
self.peer.upgrade().map(|p| p.id()).unwrap_or(0)
}
);
```
### 实现创建Channel的方法
下面我们来实现创建一个`Channel`的方法:
```rust,noplaypen
impl Channel {
#[allow(unsafe_code)]
pub fn create() -> (Arc<Self>, Arc<Self>) {
let mut channel0 = Arc::new(Channel {
base: KObjectBase::default(),
peer: Weak::default(),
recv_queue: Default::default(),
});
let channel1 = Arc::new(Channel {
base: KObjectBase::default(),
peer: Arc::downgrade(&channel0),
recv_queue: Default::default(),
});
// no other reference of `channel0`
unsafe {
Arc::get_mut_unchecked(&mut channel0).peer = Arc::downgrade(&channel1);
}
(channel0, channel1)
}
```
该方法的返回值是两端点结构体Channel的`Arc`引用,这将作为内核对象被内核中的其他数据结构引用。两个端点互相持有对方`Weak`指针这是因为一个端点无需引用计数为0只要`strong_count`为0就可以被清理掉即使另一个端点指向它。
> rust 语言并没有提供垃圾回收 (GC, Garbage Collection ) 的功能, 不过它提供了最简单的引用计数包装类型 `Rc`,这种引用计数功能也是早期 GC 常用的方法, 但是引用计数不能解决循环引用。那么如何 fix 这个循环引用呢?答案是 `Weak` 指针,只增加引用逻辑,不共享所有权,即不增加 strong reference count。由于 `Weak` 指针指向的对象可能析构了,所以不能直接解引用,要模式匹配,再 upgrade。
下面我们来分析一下这个`unsafe`代码块:
```rust,noplaypen
unsafe {
Arc::get_mut_unchecked(&mut channel0).peer = Arc::downgrade(&channel1);
}
```
由于两端的结构体将分别通过 `Arc` 引用,作为内核对象而被内核中的其他数据结构使用。因此,在同时初始化两端的同时,将必须对某一端的 Arc 指针进行获取可变引用的操作,即`get_mut_unchecked`接口。当 `Arc` 指针的引用计数不为 `1` 时,这一接口是非常不安全的,但是在当前情境下,我们使用这一接口进行`IPC` 对象的初始化,安全性是可以保证的。
### 单元测试
下面我们写一个单元测试,来验证我们写的`create`方法:
```rust,noplaypen
#[test]
fn test_basics() {
let (end0, end1) = Channel::create();
assert!(Arc::ptr_eq(
&end0.peer().unwrap().downcast_arc().unwrap(),
&end1
));
assert_eq!(end0.related_koid(), end1.id());
drop(end1);
assert_eq!(end0.peer().unwrap_err(), ZxError::PEER_CLOSED);
assert_eq!(end0.related_koid(), 0);
}
```
### 实现数据传输
Channel中的数据传输可以理解为`MessagePacket`在两个端点之间的传输,那么谁可以读写消息呢?
有一个句柄与通道端点相关联持有该句柄的进程被视为所有者owner。所以是持有与通道端点关联句柄的进程可以读取或写入消息或将通道端点发送到另一个进程。
当`MessagePacket`被写入通道时,它们会从发送进程中删除。当从通道读取`MessagePacket`时,`MessagePacket`的句柄被添加到接收进程中。
#### read
获取当前端点的`recv_queue`,从队头中读取一条消息,如果能读取到消息,返回`Ok`,否则返回错误信息。
```rust,noplaypen
pub fn read(&self) -> ZxResult<T> {
let mut recv_queue = self.recv_queue.lock();
if let Some(_msg) = recv_queue.front() {
let msg = recv_queue.pop_front().unwrap();
return Ok(msg);
}
if self.peer_closed() {
Err(ZxError::PEER_CLOSED)
} else {
Err(ZxError::SHOULD_WAIT)
}
}
```
#### write
先获取当前端点对应的另一个端点的`Weak`指针,通过`upgrade`接口升级为`Arc`指针,从而获取到对应的结构体对象。在它的`recv_queue`队尾push一个`MessagePacket`。
```rust,noplaypen
pub fn write(&self, msg: T) -> ZxResult {
let peer = self.peer.upgrade().ok_or(ZxError::PEER_CLOSED)?;
peer.push_general(msg);
Ok(())
}
fn push_general(&self, msg: T) {
let mut send_queue = self.recv_queue.lock();
send_queue.push_back(msg);
}
```
### 单元测试
下面我们写一个单元测试,验证我们上面写的`read`和`write`两个方法:
```rust,noplaypen
#[test]
fn read_write() {
let (channel0, channel1) = Channel::create();
// write a message to each other
channel0
.write(MessagePacket {
data: Vec::from("hello 1"),
handles: Vec::new(),
})
.unwrap();
channel1
.write(MessagePacket {
data: Vec::from("hello 0"),
handles: Vec::new(),
})
.unwrap();
// read message should success
let recv_msg = channel1.read().unwrap();
assert_eq!(recv_msg.data.as_slice(), b"hello 1");
assert!(recv_msg.handles.is_empty());
let recv_msg = channel0.read().unwrap();
assert_eq!(recv_msg.data.as_slice(), b"hello 0");
assert!(recv_msg.handles.is_empty());
// read more message should fail.
assert_eq!(channel0.read().err(), Some(ZxError::SHOULD_WAIT));
assert_eq!(channel1.read().err(), Some(ZxError::SHOULD_WAIT));
}
```
## 总结
在这一节中我们实现了唯一一个可以传递句柄的对象传输器——Channel我们先了解的Zircon中主要的IPC内核对象再介绍了Channel如何创建和实现read和write函数的细节。
本章我们学习了中最核心的几个内核对象,在下一章中,我们将学习`Zircon`的任务管理体系和进程、线程管理的对象。

Loading…
Cancel
Save