从 TTY 到 Wayland
660 大洋买来的 PixelBook Go 的键盘右上角是电源键,轻轻碰下就是关机。 周末 WHLUG 的时候和朋友闲聊(省略 500 字)于是想把从 TTY 进桌面这部分看一看。
在 Wayland 平台上,Compositor、Server、Manager 三位一体,后续以 WM 代称。
我之前做 WM 的时候走的是 wlroots 全权代理,系统的 base 全靠 Alpine Linux 自带的脚本:
# setup-desktop sway
脚本会自己弄好 elogind 的 daemon 和 XDG_RUNTIME_DIR 的环境变量,从来没有关心过。
本文只看 wlroots 的 DRM 后端(控制 TTY 的部分)和 libseat 的 seatd 后端。
切换 TTY 的快捷键
在实现 WM 的过程中我对 TTY 几乎没有感知,唯一的地方就是处理切换 TTY 的快捷键,它需要手动绑定:
static bool
change_vt(uint32_t modifiers, xkb_keysym_t keysym)
{
if (modifiers ^ (WLR_MODIFIER_ALT | WLR_MODIFIER_CTRL)) {
return false;
}
#define VT_LIST X(1) X(2) X(3) X(4) X(5) X(6) X(7) X(8) X(9) X(10) X(11) X(12)
int vt = 0;
switch (keysym) {
#define X(NUM) \
case XKB_KEY_XF86Switch_VT_##NUM: \
vt = NUM; \
break;
VT_LIST
#undef X
default:
return false;
}
bool changed = wlr_session_change_vt(server.session, vt);
info("input", "switched to VT %d: %s", vt, changed ? "ok" : "failed");
return changed;
}
调用的 wlroots 实际是简单的转发:
// backend/session/session.c
bool wlr_session_change_vt(struct wlr_session *session, unsigned vt) {
if (!session) {
return false;
}
return libseat_switch_session(session->seat_handle, vt) == 0;
}
libseat 与 seatd
wlroots 依赖 libseat 做 DRM 的 backend,而 libseat 就像 libinput 项目一样从 WM 项目中抽象了出来。
libseat 支持 seatd(自身独立实现)和 systemd-logind 等多个后端,负责 seat management。
其中 seatd 后端是 C/S 结构:root 身份的 daemon 在 /run/seatd.sock 监听,
代替客户端完成打开 DRM/input 设备等特权操作。
回到 TTY 的切换,libseat 把 CLIENT_SWITCH_SESSION 的 opcode 转给 seatd,daemon 去实际执行:
// seatd/client.c @handle_switch_session()
// seatd/seat.c @seat_set_next_session()
// seatd/seat.c @vt_switch()
static int vt_switch(struct seat *seat, int vt) {
int ttyfd = terminal_open(seat->cur_vt);
if (ttyfd == -1) {
log_errorf(...);
return -1;
}
terminal_set_process_switching(ttyfd, true);
terminal_switch_vt(ttyfd, vt);
close(ttyfd);
}
// common/terminal.c @terminal_switch_vt()
int terminal_switch_vt(int fd, int vt) {
log_debugf("Switching to VT %d", vt);
if (ioctl(fd, VT_ACTIVATE, vt) == -1) {
log_errorf(...);
return -1;
}
}
到这里感觉自己已经懂了,按照顺序就行。观察到 seatd 中的系统调用有:
VT_GETSTATE/VT_GETACTIVE/VT_SETMODE/VT_ACTIVATE/VT_RELDISPKDSKBMODE/KDSETMODE/HIDIOCREVOKE
回到一开始的问题,怎么从 TTY 进入到桌面(图形)环境呢?后面有兴趣再来看吧,再见👋!
补记:从 TTY 进入图形环境
连接 seatd 认领当前 TTY
在 TTY 执行 sway/tinywl,WM 调用 wlr_backend_autocreate() 和 wlr_session_create():
// backend/session/session.c @libseat_session_init()
session->seat_handle = libseat_open_seat(&seat_listener, session);
libseat 给 seatd 发送 CLIENT_OPEN_SEAT。seatd 确认 WM 启动时所在的 TTY 并修改该 TTY 的属性:
// seatd/seat.c @seat_add_client()
if (seat->vt_bound) {
seat_update_vt(seat); // VT_GETSTATE
...
client->session = seat->cur_vt;
}
// seatd/seat.c @seat_open_client() @vt_open()
// common/terminal.c @terminal_set_process_switching() 等
terminal_set_process_switching(ttyfd, true); // VT_PROCESS
terminal_set_keyboard(ttyfd, false); // KDSKBMODE = K_OFF
terminal_set_graphics(ttyfd, true); // KDSETMODE = KD_GRAPHICS
这组 ioctl 设置完成后,TTY 上的 shell 提示符消失,按键也不再有响应,显示内容完全由 WM 决定。
随后 seatd 返回 SERVER_SEAT_OPENED 并发送 SERVER_ENABLE_SEAT 事件。libseat 回调进 wlroots:
// backend/session/session.c @handle_enable_seat()
session->active = true;
wl_signal_emit_mutable(&session->events.active, NULL);
打开 GPU 和输入设备
DRM 后端初始化时枚举 KMS 设备,每个设备节点通过 wlr_session_open_file() 打开,实际是一次 RPC:
// seatd/seat.c @seat_open_device()
int fd = open(sanitized_path, O_RDWR | O_NOCTTY | ...);
case SEAT_DEVICE_TYPE_DRM:
drm_set_master(fd); // seatd 有 root
fd 经 SCM_RIGHTS 随消息传回 WM。输入设备 /dev/input/event* 走同样的通道,被 libinput 后端打开。
之后 WM 执行 atomic modeset,第一帧显示到屏幕,桌面环境的图像随之出现。
对照 elogind 路线
文章开头提到的 setup-desktop sway 走的是 elogind 路线:WM 通过 DBus 调 TakeControl/TakeDevice,
由 elogind 代劳打开设备和 VT 配置这些特权操作。殊途同归,最终落入内核的都是这几个 ioctl。