Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

从 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_RELDISP
  • KDSKBMODE/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。