Zircon内核概念

前言

Zircon内核管理着大量不同类型的Objects。这些Objects可以通过系统调用(用C++实现了Dispatcher接口的类)来直接访问。这些Objects定义在 kernel/object下面,它们有些是自包含的高级Objects,有些是对LK低级别原语的封装。

系统调用

用户态代码与内核对象通过系统调用来交互,而且基本上都是通过句柄这个概念来实现。在用户态句柄用一个32位整型数来表示(zx_handle_t类型)。执行系统调用时,内核会检查句柄参数对应的操作是否在当前调用者进程的操作表中,同时在后面也会检查句柄类型是否正确、请求的操作是否有权限执行。

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

1、调用没有限制。这类调用只占很少一部分,比如 zx_time_get()zx_nanosleep() 可以被任意线程调用。

2、调用的第一个参数是一个句柄,它作为要施加操作的对象。这类调用有很多,比如zx_channel_write()zx_port_queue()

3、调用会创建新的Objects但并不作为句柄使用,比如zx_event_create()zx_channel_create()。对Objects的访问以及对它们的限制都由调用的进程作业所控制。

libzircon.so提供了所有的系统调用,它是一个kernel提供给用户态的“虚拟”共享库(或者叫做虚拟动态共享对象,简称vDSO)。这些系统调用被表示为zx_noun_verb()zx_noun_verb_direct-object()样式的C ELF ABI函数。

所有的系统调用被定义在syscalls.sysgen 文件中,并通过 sysgen工具写入到include、libzircon以及内核的libsyscalls文件中。

权限

对象可能会有多个句柄的引用(在一个或者多个进程中)。

对大多数对象来说,当最后一个引用它的句柄关闭时,这个对象要么被废弃要么进入不可撤销的终止状态。

使用zx_channel_write()) 可以将句柄写入一个通道来实现从一个进程到另外一个进程的移动或者使用zx_process_start() 将一个句柄作为参数传递新进程的第一个线程。这些行为会受到句柄、句柄所引用对象的权限约束,同一对象的两个句柄也可能具有不同的权限。

zx_handle_duplicate()zx_handle_replace() 系统调用可以获取传入对象的额外句柄,也可以削弱其权限。zx_handle_close() 系统调用会关闭一个句柄,如果这个句柄是引用对象的最后一个句柄,那么也会释放这个对象。

内核对象ID

每个在内核中的对象都有一个“内核对象id”(简称koid)。koid用一个64位的无符号整型来表示,它在整个运行时来唯一的标识这个对象,这意味着koid从来不会被重用。

koid中有两个特殊值:ZX_KOID_INVALID-值为0,用来表示null。ZX_KOID_KERNEL-值为1。

运行时:作业、进程、线程

线程代表了线程在其所属进程地址空间的执行(寄存器、栈等)。进程属于作业,它定义许多资源的限制条件。作业从属于父作业,所有的作业又都从属于内核启动后传递给userboot的第一个用户态进程的根作业。

进程内的线程如果要创建一个新的进程或者作业,必须通过作业句柄来实现。

程序加载由内核层之上的用户态设备、协议来提供。

更多参考:进程创建, 进程启动, 线程创建, 线程启动

消息传递: 套接字、通道

套接字和通道都是支持双向传递的IPC对象。创建一个套接字或者通道会返回两个连接每一末端对象的句柄。

套接字是面向流的,支持读/写一个或者多个字节,短写(套接字缓冲区满)和短读(请求到的数据多于套接字缓冲区)都有可能发生。

通道是面向数据包的,消息最大长度为64K字节(可以调整,通常会设置的更小),支持1024个(可以调整,通常会设置的更小)句柄附加到消息上。不管消息能不能适应大小,通道都不支持短写和短读。

当句柄写入到通道时它们就会从发送端进程中移除,相反含有句柄的消息从通道中读出时句柄会被追加到接收进程。在这两个事件之间,句柄会持续存在(前提是对象引用持续存在),如果句柄写入方向的通道关闭,那么消息就会被废弃,所有的句柄都会被关闭。

更多参考:

channel_create, channel_read, channel_write, channel_call, socket_create, socket_read, socket_write.

对象、信号

对象有32种信号(zx_signals_t类型,定义在ZX_SIGNAL文件中),信号代表当前对象的一系列的状态信息。比如通道、套接字有可读、可写状态,进程、线程有终止状态等等。

线程可能在一个或者多个对象上等待信号让它变为活动状态。

更多参考: signals

等待: 等待一个、等待多个、端口

线程可以使用 zx_object_wait_one() 在一个句柄上等待一个信号变为活动状态,也可以使用 zx_object_wait_many() 在多个句柄上等待信号。这两个系统调用都允许设置超时时间,即便信号没有被发送也可以返回。

如果一个线程在大量的句柄上等待,使用端口会更有效率。端口是一个对象,它被众多声明了信号的对象绑定。端口会接收一个关于发送信号信息的包。

更多参考: port_create, port_queue, port_wait, port_cancel.

事件、事件对

事件是最简单的对象,除了活动信号集外没有其他状态。

事件对是一对可以互相发信号的事件组合。事件对一个有用的场景是当事件对的一方离开(所有的句柄都被关闭)时,事件对的另一方就被置为PEER_CLOSED信号。

更多参考: event_create, and eventpair_create.

共享内存:虚拟内存对象(VMOs)

虚拟内存对象代表了一组内存物理页或者内存物理页的操纵能力(创建、填充、请求)。

zx_vmar_map() 系统调用将VMO映射到进程的地址空间。zx_vmar_unmap() _系统调用将VMO从进程的地址空间中解映射。映射页的权限可以通过 [zx_vmar_protect()_ ](https://fuchsia.googlesource.com/zircon/+/HEAD/docs/syscalls/vmar_protect.md)来调整。

VMO也可以通过 zx_vmo_read()zx_vmo_write() 来直接读取、写入,这样可以避免映射VMO到进程地址空间的耗时操作(比如创建VMO、写数据集到内部、传递给其他线程来使用)。

地址空间管理

虚拟内存地址区(VMARs)提供了管理进程地址空间的一层抽象,在进程创建时会传递给创建者一个根VMAR的句柄,这个句柄是整个地址空间VMAR的引用。整个地址空间可以通过 zx_vmar_map() 系统调用来切分,通过 zx_vmar_allocate() 来生成新的VMAR(子分区),这个分区可以是部分地址空间的组合。

更多参考: vmar_map, vmar_allocate, vmar_protect, vmar_unmap, vmar_destroy,

互斥体

互斥体是与用户态原子操作一起使用的内核原语,用来实现高效的同步原语。比如Mutex仅在竞争情况下才会做系统调用。一般情况下标准库才对互斥体感兴趣。Zircon的libc、libc++提供了C11、C++、Pthread API版本的Mutex、条件变量。

更多参考: futex_wait, futex_wake, futex_requeue.

Zircon设备索引

Zircon设备索引是从bootloader传递过来的只读二进制数据结构,它包含了内核以及众多驱动的配置信息。

详情参见 zircon设备索引