简介
内核管理多种不同类型的对象。可通过系统调用直接访问的 Dispatcher 是实现 Dispatcher 接口的 C++ 类。这些方法在 kernel/object 中实现。其中许多都是自包含的高级对象。lk
系统调用
用户空间代码通过系统调用与内核对象交互,并且几乎完全通过句柄进行交互。在用户空间中,句柄表示为 32 位整数(类型为 zx_handle_t)。执行系统调用时,内核会检查句柄参数是否引用调用进程的句柄表中存在的实际句柄。内核会进一步检查句柄的类型是否正确(将线程句柄传递给需要事件句柄的系统调用将导致错误),以及句柄是否具有所请求操作所需的权限。
从访问权限的角度来看,系统调用分为三大类:
- 没有任何限制的调用(只有极少数),例如
zx_clock_get_monotonic()
和zx_nanosleep()
,可以由任何线程调用。 - 将句柄作为第一个参数的调用,表示它们要操作的对象,这类调用占绝大多数,例如
zx_channel_write()
和zx_port_queue()
。 - 创建新对象但不接受句柄的调用,例如
zx_event_create()
和zx_channel_create()
。对这些资源的访问权限(以及对这些资源的限制)由包含调用进程的作业控制。
libzircon.so 提供系统调用,它是 Zircon 内核向用户空间提供的“虚拟”共享库,也称为虚拟动态共享对象或 vDSO。它们是格式为 zx_noun_verb()
或 zx_noun_verb_direct-object()
的 C ELF ABI 函数。
系统调用在 //zircon/vdso 中以自定义的 FIDL 形式定义。这些定义首先由 fidlc
处理,然后由 zither
处理,后者会从 fidlc
获取 IR 表示法,并输出在 VDSO、内核等中用作粘合剂的各种格式。
标识名和权利
对象可能具有多个引用它们的句柄(在一个或多个进程中)。
对于几乎所有对象,当引用某个对象的最后一个打开的句柄关闭时,该对象会被销毁或置于无法撤消的最终状态。
您可以通过将句柄写入通道(使用 zx_channel_write()
)或使用 zx_process_start()
将句柄作为新进程中第一个线程的参数传递,将句柄从一个进程移至另一个进程。
系统会根据与标识名关联的权限来决定可以对标识名或其所引用的对象执行哪些操作。两个指向同一对象的句柄可能具有不同的权限。
zx_handle_duplicate()
和 zx_handle_replace()
系统调用可用于获取与传入的句柄引用同一对象的其他句柄,可选地具有较少的权限。zx_handle_close()
系统调用会关闭句柄,并释放其引用的对象(如果该句柄是该对象的最后一个句柄)。zx_handle_close_many()
系统调用同样会关闭句柄数组。
内核对象 ID
内核中的每个对象都有一个“内核对象 ID”(简称“koid”)。 它是一个 64 位无符号整数,可用于标识对象,并且在运行系统的生命周期内是唯一的。这尤其意味着,koid 绝不会重复使用。
有两个特殊的 koid 值:
ZX_KOID_INVALID 的值为零,并用作“null”哨兵。
ZX_KOID_KERNEL 只有一个内核,并且它有自己的 koid。
内核生成的 koid 仅使用 63 位(足够了)。这样可以通过设置最有显著性的位来为人为分配的 koid 留出空间。内核生成的 koid 的分配顺序未指定,可能会发生变化。
人工 koid 用于支持识别人工对象(例如跟踪中的虚拟线程),以供工具使用。人工 koid 的分配方式由各个程序决定,本文档不会强加任何规则或惯例。
运行代码:作业、进程和线程。
线程表示位于其所在进程拥有的地址空间中的执行线程(CPU 寄存器、堆栈等)。进程归作业所有,作业定义了各种资源限制。作业由父级作业拥有,一直到根作业,该作业由内核在启动时创建并传递给 userboot
(开始执行的第一个用户空间进程)。
如果没有作业句柄,进程中的线程将无法创建其他进程或其他作业。
程序加载由内核层之上的用户空间设施和协议提供。
请参阅:zx_process_create()
、zx_process_start()
、zx_thread_create()
和 zx_thread_start()
。
消息传递:套接字和通道
套接字和通道都是双向的双端 IPC 对象。创建套接字或通道会返回两个句柄,每个句柄都引用对象的每个端点。
套接字是面向流的,数据可以以一个或多个字节为单位写入或读出。可能发生短写入(如果套接字的缓冲区已满)和短读取(如果请求的数据量超过缓冲区中的数据量)。
通道以数据报为导向,消息大小上限由 ZX_CHANNEL_MAX_MSG_BYTES 给出,并且最多可以有 ZX_CHANNEL_MAX_MSG_HANDLES 个句柄附加到消息。它们不支持短读取或写入,消息要么适合,要么不适合。
将句柄写入通道后,它们会从发送进程中移除。从通道读取包含句柄的消息时,系统会将句柄添加到接收进程中。在这两种事件之间,句柄会继续存在(确保它们引用的对象继续存在),除非它们写入的通道的末尾已关闭,此时系统会丢弃传输到该端点的消息,并关闭其中包含的所有句柄。
请参阅:zx_channel_create()
、zx_channel_read()
、zx_channel_write()
、zx_channel_call()
、zx_socket_create()
、zx_socket_read()
和 zx_socket_write()
。
对象和信号
对象最多可以有 32 个信号(由 zx_signalst 类型和 ZXSIGNAL 定义表示),这些信号表示与其当前状态相关的信息。例如,通道和套接字可以是可读或可写的。进程或线程可能会被终止。等等。
线程可能会等待信号在一个或多个对象上变为有效。
如需了解详情,请参阅信号。
等待:等待一个、等待多个和端口
线程可以使用 zx_object_wait_one()
等待单个句柄上的信号处于活动状态,也可以使用 zx_object_wait_many()
等待多个句柄上的信号。这两种调用都支持超时,超时后它们会返回,即使没有待处理的信号也是如此。
超时时间可能会因计时器空闲时间而与指定的截止期限有所偏差。如需了解详情,请参阅计时器松弛时间。
如果线程要等待一组大型句柄,则使用端口更高效。端口是一种对象,其他对象可以绑定到该对象,这样当对这些对象断言信号时,端口会收到包含有关待处理信号的信息的数据包。
请参阅:zx_port_create()
、zx_port_queue()
、zx_port_wait()
和 zx_port_cancel()
。
事件、事件对。
事件是最简单的对象,除了其收集的有效信号之外,没有其他状态。
事件对是指一对可能相互发送信号的事件。事件对的一个实用属性是,当对的一侧消失(对应的所有句柄均已关闭)时,另一侧会断言 PEER_CLOSED 信号。
请参阅 zx_event_create()
和 zx_eventpair_create()
。
共享内存:虚拟内存对象 (VMO)
虚拟内存对象代表一组物理内存页面,或页面的潜在用途(将按需延迟创建/填充)。
它们可以使用 zx_vmar_map()
映射到进程的地址空间,并使用 zx_vmar_unmap()
取消映射。您可以使用 zx_vmar_protect()
调整映射页面的权限。
您还可以使用 zx_vmo_read()
和 zx_vmo_write()
直接读取和写入 VMO。因此,对于“创建 VMO、将数据集写入其中,并将其交给另一个进程使用”等一次性操作,可以避免将它们映射到地址空间的开销。
地址空间管理
虚拟内存地址区域 (VMAR) 提供了一个用于管理进程地址空间的抽象。在进程创建时,系统会将根 VMAR 的句柄提供给进程创建者。该句柄是指跨整个地址空间的 VMAR。此空间可以通过 zx_vmar_map()
和 zx_vmar_allocate()
接口进行划分。zx_vmar_allocate()
可用于生成新的 VMAR(称为子区域或子项),可用于将地址空间的各部分分组在一起。
请参阅:zx_vmar_map()
、zx_vmar_allocate()
、zx_vmar_protect()
、zx_vmar_unmap()
和 zx_vmar_destroy()
Futex
Futex 是内核基元,可与用户空间原子操作搭配使用,以实现高效的同步基元,例如仅在争用情况下需要进行系统调用的互斥量。通常,只有标准库的实现者才会对它们感兴趣。Zircon 的 libc 和 libc++ 为以 Futex 实现的互斥量、条件变量等提供了 C11、C++ 和 pthread API。
请参阅:zx_futex_wait()
、zx_futex_wake()
和 zx_futex_requeue()
。