Zircon 内核概念

简介

内核管理着许多不同类型的对象。可通过系统调用直接访问的类是实现 Dispatcher 接口的 C++ 类。这些是在 kernel/object 中实现的。许多对象都是独立的更高级别对象。

系统调用

用户空间代码通过系统调用与内核对象交互,并且几乎仅通过句柄与内核对象进行交互。在用户空间中,句柄表示为 32 位整数(类型 zx_handle_t)。执行系统调用时,内核会检查句柄参数是否引用了调用进程的句柄表中的实际句柄。内核会进一步检查句柄的类型是否正确(将线程句柄传递给需要事件句柄的系统调用将导致错误),并且句柄是否具有执行请求操作所需的权限。

从访问的角度来看,系统调用分为三大类:

  1. 没有任何限制的调用(只有极少数的调用),例如 zx_clock_get_monotonic()zx_nanosleep() 可以由任何线程调用。
  2. 以句柄为第一个参数的调用,表示它们操作的对象(绝大多数),例如 zx_channel_write()zx_port_queue()
  3. 可创建新对象但不采用句柄的调用,例如 zx_event_create()zx_channel_create()。对这些项(及其限制)的访问由包含调用进程的 Job 控制。

系统调用由 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 位的无符号整数,可用于识别对象,并且在运行的系统的生命周期内具有唯一性。也就是说,我们绝不重复使用 Kids。

有两个特殊的 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 定义表示),这些信号代表一条有关其当前状态的信息。例如,通道和套接字可以是 READABLE 或 WRITABLE。进程或线程可能会被终止。依此类推。

线程可以等待一个或多个对象的信号变为活动状态。

如需了解详情,请参阅信号

等待:等待 1、等待多次和携号转网

线程可以使用 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++ 为互斥量、条件变量等提供了 C11、C++ 和 pthread API,并且通过 Futexe 实现。

请参阅:zx_futex_wait()zx_futex_wake()zx_futex_requeue()