这是整个 Arcan 项目中使用的 IPC 系统的技术描述,从设计师和开发者的角度出发,沿途对遗留问题和考虑因素进行了注释。它是 Arcan 生态系统中的几个瑰宝之一,仅在这方面就投入了数千小时的工作。
历史
SHMIF(“共享内存接口”)有着悠久的历史,可追溯到 2007 年左右。最初是为了满足在主引擎中对不信任数据的所有解析进行最小特权分离的需求而添加的,因为 ffmpeg 库无法阻止内存损坏——这通常是一件坏事,而且我们从 GPU 驱动程序的工作中已经有了足够多的这种情况。
随着解析器被沙盒化,它演变成一种链接器插入或注入的 shellcode 方式,用于在不被发现的情况下操纵第三方音频/视频处理和事件循环。有传言称它曾被用于自动化《魔兽世界》等游戏中的许多繁琐工作,且未被发现。
它被编写为可在许多操作系统上移植。最初的版本在 Windows、OSX、BSD 和 Linux 上运行。还有在 Android 和 iOS 上运行的非公开版本。如今,重点仍然放在 BSD 和 Linux 上,同一模型的网络版本“A12”旨在与其他版本保持兼容性。
其设计基于从模拟旧时街机游戏中吸取的教训,因为它们代表了迄今为止最多样化和最复杂的显示系统。数据模型从越来越复杂的实验中演变而来,甚至超越了仔细检查 X11 中每个调度函数以确保不遗漏任何内容的阶段。安全和恢复方面来自为电网控制系统进行故障排除和修复的许多教训。调试和性能选择来自在(主要是)Android 上的最后手段调试老虎团队工作。
布局
有一个共享内存区域和一组特定于操作系统的原语,以解决各种内核在暴露内存分配和使用控制方面的不足。我们将这些统称为一个段。建立的第一个段称为“主”段,在成功连接时是唯一保证的段。其他段是可协商的,默认情况下拒绝任何新的分配。此决策最终由窗口管理策略决定。
共享内存分为固定的静态区域和动态区域。
静态区域的字段顺序是有机的,随着时间的推移不断扩展。为避免破坏兼容性,在需要更多元数据时会添加更改。标记为“aux”的区域默认大小为 0,仅用于协商高级功能,如 HDR 元数据和 VR 设备支持。
静态区域中一些更相关和不明显的成员包括:
- DMS - 死亡开关:如果它被修改,该段将被视为死亡。在这一点之后,对该页面的任何修改都将不会被另一方处理。(见“安全措施”部分)
- 验证 cookie:这是一个校验和,通过计算该区域中其他成员的偏移量和值组成。双方定期计算并比较此值,以检测版本不匹配或损坏。
- 入站/出站事件缓冲区:这些是 128b 事件的固定槽环形缓冲区。可以将它们视为异步“系统”调用(见“事件处理”部分)。
- 段令牌:此特定段的唯一标识符。如果标识符已通过其他机制共享,客户端可以使用此标识符引用其他事件。例如,“VIEWPORT”事件指示窗口管理重新定位或嵌入其他客户端或线程拥有的段。
整个内存区域被视为一个不安全的竞争区域;一方用它想要进行的更改填充它,并通过一些同步触发器,另一方验证并应用或拒绝它们。
对于调试和检查,这意味着单个映射内存范围的快照足以检查连接状态,并且编写分析、模糊测试和报告工具非常简单。
原始布局不一定暴露给相应库的使用者。相反,一个上下文结构(struct arcan_shmif_cont)包含指向相应子区域的开发人员相关指针。
虽然此接口的实现位于用户空间,但设计意图是能够让服务器端完全位于内核中,并将其用作唯一的系统调用接口。
每个段都有一个类型,在 REGISTER 事件期间(或通过 SEGREQ 事件请求新段时)从客户端传输到服务器。这主要是窗口管理和服务器提示,用于控制事件响应,但也决定视频和音频缓冲区是用于(默认)客户端到服务器还是(屏幕录制和类似功能)服务器到客户端。
首次连接(客户端)
客户端以库libarcan-shmif的形式出现。大致的框架如下:
#include <arcan_shmif.h>
int main(int argc, char **argv)
{
struct arg_arr args;
struct arcan_shmif_cont C =
arcan_shmif_open(SEGID_APPLICATION,
SHMIF_ACQUIRE_FATALFAIL,
&args);
struct arcan_shmif_initial *config;
arcan_shmif_initial(&C, &config);
/* 发送音频/视频 */
/* 事件处理 */
arcan_shmif_drop(&C);
}SEGID_部分是给服务器的一个提示,说明此连接的预期用途以及服务器如何管理其资源分配和调度。有几种类型可用,其中APPLICATION是一个安全的通用类型。视频播放器最好使用MEDIA(对同步非常敏感,但不输入),而游戏则使用GAME(资源利用率高、输入延迟和最近的呈现比“完美”帧更重要)。FATALFAIL部分表示如果无法协商连接则无需继续。它节省了一些错误检查并统一了类似'fprintf(stderr, "Couldn't connect")'的操作。arg_arr 'args'用于在不破坏传统getopt/argv的情况下将命令行参数传递给客户端。可以使用if (arg_lookup(&args, "myopt", 0, &val)){...}来检查键值对。
客户端如何知道找到服务器取决于操作系统,但对于 POSIX 情况,库正在寻找两个主要选项:ARCAN_CONNPATH和ARCAN_SOCKIN_FD环境变量。CONNPATH的值是连接点的名称,由服务器端定义。'arcan_shmif_initial'部分提供了创建第一帧正确内容所需的信息。这包括用户首选的文本格式(大小、提示)、输出密度(DPI 感知绘图以避免“缩放”愚蠢行为)、配色方案(可访问性对比度、色盲或亮/暗),甚至区域设置(从LC_… /getlocale迁移)和位置(纬度/经度/海拔)。
对于加速图形,它还包含用于渲染的 GPU 设备的引用,这允许服务器隔室或在多个加速器之间进行负载平衡。
发送音频/视频的部分:
shmif_pixel px = SHMIF_RGBA(0x00, 0xff, 0x00, 0xff);
for (size_t y = 0; y < C.h; y++)
for (size_t x = 0; x < C.w; x++)
C.vidp[Y * C.pitch + x] = px;
arcan_shmif_signal(C, SHMIF_SIGVID);这用线性 RGB 中的不透明绿色像素填充段的动态视频缓冲区部分,像素格式为系统认为的原生格式(嵌入在SHMIF_RGBA宏中)。
在大多数系统上(如今)这将是 32 位 RGBA,但在 CPU 端序方面将其视为编译时原生格式。低端嵌入式设备可能希望使用 RGB565,像 eInk 这样的特殊设备可能希望使用 RGB800 等。
这里有很多选项,但大多数都需要处理特殊的同步或缓冲需求,这些在“同步”和“加速图形”的“特殊情况”部分中有所介绍。
对于音频表示,模式相同,将vidp替换为audp,将SHMIF_SIGVID替换为SHMIF_SIGAUD(这是一个位掩码,如果两者都有则使用)。
我们省略了音频表示,但对于事件处理部分:
struct arcan_event ev;
while (arcan_shmif_wait(&C, &ev)){
if (ev.category == EVENT_IO){
/* 鼠标、键盘、眼动追踪等处理在此处 */
}
switch(ev.tgt.kind){
case TARGET_COMMAND_EXIT:
/* 任何自定义清理在此处 */
break;
case TARGET_COMMAND_RESET:
/* 假设我们回到了开始的状态 */
default:
break;
}
}此代码将阻塞直到接收到事件,更多选项在“同步”部分中介绍。我们只需获得礼貌的建议“如果您对此做些什么就好了”。category部分将仅为EVENT_IO或EVENT_TARGET,下一部分将解释原因。_RESET事件特别有趣,将在“恢复”特殊情况中介绍。它可以由外部桌面出于任何原因启动,只是建议“回到您的起始状态,我已经忘记了一切”,但也用于服务器崩溃并恢复或正在关闭并已将责任移交给另一个服务器的情况。
事件数据模型涵盖 32 种服务器到客户端的可能性和 22 种客户端到服务器的可能性。它们涵盖了整个桌面所需的一切,但只是描述性的,不是规范性的。响应与您相关的事件,忽略其他事件。
首次连接(服务器)
服务器端有两个实现;一个在 arcan 代码库内部,更适合其更高级的资源管理、崩溃恢复和脚本运行时。另一个作为库libarcan-shmif-server出现,主要用于 arcan-net 网络工具,该工具将其转换为 A12 网络协议。
创建一个客户端连接点的示例:
#include <arcan_shmif.h>
#include <arcan_shmif_server.h>
struct shmifsrv_client *cl =
shmifsrv_allocate_connpoint("demo", NULL, S_IRWXU, fd);
shmifsrv_monotonic_rebase();这创建了一个客户端可以绑定的连接点。还有shmifsrv_spawn_client和shmifsrv_inherit_connection两种替代方法。spawn负责创建一个带有内部原语的进程。inherit使用一些预先存在的原语并从那里构建。两者都返回相同的shmifsrv_client结构。shmifsrv_monotonic_rebase()调用设置了内部计时,以提供粗略(25Hz)的 CLK(时钟)信号。
处理与渲染/输入处理循环交错的事件是一个更大的主题,此处不做介绍。
int status;
while ((status = shmifsrv_poll(cl) <= CLIENT_NOT_READY)
{
/* 事件和缓冲区处理在此处 */
}
if (status == CLIENT_DEAD)
{
shmifsrv_free(cl, SHMIFSRV_FREE_FULL);
exit(EXIT_SUCCESS);
}可以提取用于 I/O 多路复用的特定于操作系统的标识符,以便仅在有一些入站数据通过shmifsrv_client_handle()发出信号时才调用_poll。_free传递的标志确定客户端可见性。可以仅释放服务器端资源而不触发死亡开关,这可用于将客户端透明地传递给另一个 shmif 服务器进程或实例。
在处理事件和缓冲区之前,还需要管理外部的计时。
int left;
int ticks = shmifsrv_monotonic_tick(&left);
while (ticks--){
shmifsrv_tick(cl);
}这提供了完整性和活跃度检查,并管理客户端请求的计时器。(left)返回下一个滴答之前的毫秒数。这用作更高级调度程序的反馈(如果有)。
事件处理部分:
struct arcan_event buf[64];
size_t n_events, index = 0;
if ((n_events = shmifsrv_dequeue_events(cl, buf, 64)){
while (index!= n_events){
struct arcan_event* ev = &buf[index++];
if (shmifsrv_process_event(cl, ev))
continue;
/* 事件处理程序在此处 */
}
}这将最多 64 个事件出队到缓冲区中。每个事件都被转发回库,以允许内部管理的子集。它们只是通过开发人员路由,以提供完全的可见性。可以使用arcan_shmif_eventstr()获取其内容的人类可读表示。
限制事件数量是为了防止恶意客户端设置情况,以作为拒绝服务的一部分来阻止服务器或耗尽其文件描述符空间,无论是直接影响用户还是作为利用链的一部分。
if (ev->category!= EVENT_EXTERNAL){
fprintf(stderr, "unexpected event category\n");
continue;
}
switch (ev->ext.kind){
case EVENT_EXTERNAL_REGISTER:
/* 仅在客户端上允许一次 */
arcan_shmifsrv_enqueue(cl, &(struct arcan_event){
.category = TARGET_COMMAND,
.tgt = {
.kind = TARGET_COMMAND_ACTIVATE
}
}
break;
default:
break;
}事件数据模型有很多显示服务器特定的细节,这里只介绍了一个。这使客户端从“预滚动”状态中解放出来,在该状态下,它将接收到的信息累积到“arcan_shmif_initial”结构中,如客户端部分所述。客户端产生正确第一帧所需的任何信息都在“ACTIVATE”之前。最可能需要的是DISPLAYHINT、OUTPUTHINT和FONTHINT,以指示它将缩放的大小、呈现的密度、颜色空间和子通道布局,以及最重要的原语(文本)的首选大小。
最后一部分是处理之前代码的“缓冲区处理”部分。
/* 单独的函数 */
bool audio_cb(shmif_asample *buf,
size_t n_samples,
unsigned channels, unsigned rate,
void *tag)
{
/* 将 buf[n_samples] 转发到配置为处理 [channels] 在 [rate] 的音频设备或混音器 */
return true;
}
if (status & CLIENT_VBUFFER_READY){
struct shmifsrv_vbuffer vbuf = shmifsrv_video(cl);
/* 将 vbuf 内容转发到 GPU */
shmifsrv_video_step(cl);
}
if (status & CLIENT_ABUFFER_READY){
shmifsrv_audio(cl, audio_cb, NULL);
}vbuf的内容很复杂,包含原始缓冲区或不透明的 GPU 系统句柄+元数据(定时、脏区域等),或 TPACK(见“仅文本窗口”部分)以及与“呈现提示”对应的一组标志,关于坐标系、alpha 混合等如何解释缓冲区内容。
大多数图形处理属性是任何有能力的扫描输出引擎都有硬件加速的东西,除了 TPACK(甚至古老的图形适配器也曾经将其作为“文本模式”显示分辨率)。有支持函数在“arcan_tui.h”中将其解包为紧凑的文本行列表及其着色和格式,即arcan_tui_tunpack()。
对于加速 GPU 句柄,可以通过发送BUFFERFAIL事件拒绝它。这将迫使客户端实现将加速的 GPU 本地内容转换为其端的共享像素格式。这在“特殊情况:加速图形”中进一步介绍。它还作为一种安全措施,防止客户端向 GPU 提交永远不会完成的命令缓冲区并以此方式造成活锁组合(或利用 GPU 端的许多漏洞)。在强化系统中,这将与 IO-MMU 隔离一起使用。
同步
“显示服务器”的真正工作实际上是具有软实时约束的桌面 IPC,而不是提供对显示器的访问。同步是此类系统的核心。如果未能意识到这一点,将会遇到很多问题。
推荐学习“排队理论”和“信号处理”以获得更深入的理解。
在客户端代码示例中:
arcan_shmif_signal(&C, SHMIF_SIGVID);这是同步且阻塞的。线程将不会继续执行,直到服务器端表示可以(通过shmifsrv_video_step代码)。服务器可以推迟此操作以优先处理其他客户端或错开释放以减轻“奔牛问题”。
对于普通应用程序,这通常足够,与“VSYNC”相当。当有更严格的延迟要求和/或生成一帧成本很高时,需要更多措施。历史上“更简单”的解决方案是添加另一个缓冲区:
arcan_shmif_resize_ext(&C, C.w, C.h,
(struct shmif_resize_ext){
.vbuf_cnt = 2
});_resize和_resize_ext调用都是同步且阻塞的,因为服务器端需要控制以保证有足够的可用内存并获得许可。它将重新计算上下文中的所有缓冲区偏移量(vidp、audp 等)和验证 cookie,并可能移动基地址以满足虚拟或物理内存约束。
某些加速显示扫描输出控制器对物理上连续的固定线性地址的内存有严格要求,这些是有限且稀缺的资源,这就是此类 resize 请求可能失败的原因,尤其是在紧凑的嵌入式开发环境中。在处理虚拟机中的虚拟 GPU 等情况下也是如此。另一种满足请求的方法是在服务器端缓冲,这会导致额外的复制,增加功耗并减少可用于其他用途的内存带宽。
增加缓冲区计数可以改变语义,以指示仅应考虑最近提交的缓冲区,其他缓冲区可以丢弃。这以牺牲内存消耗为代价解决了双缓冲的延迟问题。历史上这被称为“三缓冲”,但由于也用于“双缓冲”行为且只是更深的缓冲区队列,因此变得毫无意义。
并非缓冲区的每个部分都实际发生了变化。一种常见的优化是注释应考虑的区域,对于常规 UI
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。