得之我幸 失之我命

when someone abandons you,it is him that gets loss because he lost someone who truly loves him but you just lost one who doesn’t love you.

Linux TTY 熄屏

问题描述

这台机器环境如下:

  • Intel MacBook Pro,带 Apple T2 芯片
  • Debian Trixie 系统
  • T2 Linux 内核:7.2.3-1-t2-trixie
  • Intel 核显,使用 i915
  • 没有运行桌面环境,主要使用 Linux TTY,本机显示的是 tty1
  • 日常主要通过 SSH 管理机器

因为机器大部分时间作为服务器运行,没有必要让 MacBook 内置屏幕一直亮着

目标有两个:

  1. tty1 长时间没人操作时自动熄屏;
  2. 需要的时候可以立即熄屏,而且之后按一下本机键盘就能重新亮屏

解决方案

目标 1:tty1 空闲 1 分钟后自动熄屏

Linux virtual console 本身支持 blank timer,可以直接使用 setterm

1
2
3
4
5
6
7
8
9
10
11
12
13
# 直接设置
$ sudo sh -c 'TERM=linux setterm --blank 1 < /dev/tty1 > /dev/tty1'
# --blank 1 空闲 1 分钟后熄屏
# TERM=linux 操作 Linux virtual console
# /dev/tty1 通过 SSH 执行时,明确操作本机 tty1

# 检查当前设置
$ sudo sh -c 'TERM=linux setterm --blank < /dev/tty1'
1 # 当前 blank timeout 为 1 分钟

# 也可以查看内核记录的 console blank 时间
$ cat /sys/module/kernel/parameters/consoleblank
60 # 单位为秒,对应 1 分钟

使用 systemd 开机自动设置

上面的设置只需要执行一次,但重启以后需要重新应用,因此创建一个 systemd service

创建 /etc/systemd/system/tty1-blank.service

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Enable console blanking on tty1
After=getty@tty1.service # 等 tty1 的 getty 启动后再设置

[Service]
Type=oneshot # 只需要执行一次
ExecStart=/bin/sh -c 'TERM=linux /usr/bin/setterm --blank 1 < /dev/tty1 > /dev/tty1'
RemainAfterExit=yes # 执行完成后仍将 service 视为 active

[Install]
WantedBy=multi-user.target

启用:

1
2
3
4
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now tty1-blank.service
# enable 开机自动执行
# --now 当前这次启动立即执行

检查:

1
2
3
4
5
6
7
$ systemctl status tty1-blank.service
...
Active: active (exited)

# 这里的 active (exited) 是正常状态,并不是服务挂了
# 因为它是 Type=oneshot,任务只是执行一次 setterm
# 命令执行完成后进程自然退出,而 RemainAfterExit=yes 让 systemd 继续将其标记为 active

如果屏幕已经自动熄灭,需要从 SSH 手动点亮:

1
2
$ sudo sh -c 'TERM=linux setterm --blank poke < /dev/tty1 > /dev/tty1'
# --blank poke 唤醒 console,并重新开始计算空闲时间

目标 2:立即熄屏,同时允许键盘唤醒

使用 framebuffer 的 FBIOBLANK ioctl:

1
2
3
4
$ sudo python3 -c 'import os,fcntl; fd=os.open("/dev/fb0",os.O_RDWR); fcntl.ioctl(fd,0x4611,1); os.close(fd)'
# /dev/fb0 当前 framebuffer 设备
# 0x4611 FBIOBLANK ioctl
# 1 FB_BLANK_NORMAL

执行后屏幕立即黑掉,按 MacBook 本地键盘,屏幕会自动重新亮起


目标 2 的 Debug 过程

目标 1 使用 setterm --blank 1 就可以实现,没有遇到什么问题

问题主要出现在实现“立即熄屏”的过程中

1. setterm --blank force 可以熄屏,但键盘无法唤醒

最开始想到的是 setterm 提供的 force

1
2
$ sudo sh -c 'TERM=linux setterm --blank force < /dev/tty1 > /dev/tty1'
# --blank force 立即强制 blank

执行以后,屏幕确实立即黑掉

但很快发现:

按 MacBook 本地键盘无法重新点亮屏幕

只能通过 SSH 执行:

1
2
$ sudo sh -c 'TERM=linux setterm --blank poke < /dev/tty1 > /dev/tty1'
# poke 显式解除 blank

才能恢复

这里出现了第一个问题:

为什么 --blank 1 自动熄屏以后键盘可以唤醒,而 --blank force 却不可以?


2. 先排除键盘本身的问题

首先确认 Linux 当前识别到了哪些 input event:

1
2
3
4
5
6
7
$ grep -H . /sys/class/input/event*/device/name

/sys/class/input/event0/device/name:Lid Switch
...
/sys/class/input/event8/device/name:Apple Inc. Apple Internal Keyboard / Trackpad
/sys/class/input/event10/device/name:Apple Inc. Apple Internal Keyboard / Trackpad
...

event8event10 名字相同,所以进一步确认它们分别是什么设备:

1
2
3
4
5
6
7
8
9
10
$ udevadm info -q property -n /dev/input/event8 | grep '^ID_INPUT'
ID_INPUT=1
ID_INPUT_KEY=1
ID_INPUT_KEYBOARD=1 # event8 是键盘

$ udevadm info -q property -n /dev/input/event10 | grep '^ID_INPUT'
ID_INPUT=1
ID_INPUT_TOUCHPAD=1 # event10 是触控板
ID_INPUT_WIDTH_MM=135
ID_INPUT_HEIGHT_MM=84

因此监听 event8

1
2
3
$ sudo evtest /dev/input/event8
# evtest 直接观察 Linux input subsystem 收到的输入事件
# event8 MacBook 内置键盘

保持 evtest 运行,再从另一个 SSH 会话执行:

1
$ sudo sh -c 'TERM=linux setterm --blank force < /dev/tty1 > /dev/tty1'

等屏幕黑掉后,按本地键盘,此时 evtest 可以看到类似:

1
Event: time ..., type 1 (EV_KEY), code ..., value 1

也就是说:

force 熄屏以后,键盘事件仍然正常进入 Linux input subsystem。

所以问题不是:

1
2
3
屏幕熄灭

键盘失效

而是:

1
2
3
4
5
屏幕熄灭

键盘事件正常进入内核

没有触发 console unblank

因此可以排除 T2 键盘驱动和键盘设备本身


3. 确认当前操作的就是 tty1

接着确认当前屏幕实际显示的是哪个 virtual terminal:

1
2
$ cat /sys/class/tty/tty0/active
tty1 # 当前 active VT 确实是 tty1

这排除了另一个可能:

命令操作的是 /dev/tty1,但屏幕实际显示的是其他 VT


4. 检查 fbcon 是否提供 blank 接口

Linux framebuffer console 的 sysfs 位于:

1
2
3
4
5
6
7
$ ls -la /sys/class/graphics/fbcon/
...
-rw-r--r-- ... cursor_blink
drwxr-xr-x ... power
-rw-r--r-- ... rotate
--w------- ... rotate_all
...

这里并没有一些资料中可以看到的 /sys/class/graphics/fbcon/blank,所以这台机器不能通过这个 sysfs 节点直接触发 framebuffer blank

继续查看 graphics 设备:

1
2
3
$ ls -l /sys/class/graphics/
fb0 -> ../../devices/pci0000:00/0000:00:02.0/graphics/fb0
fbcon -> ../../devices/virtual/graphics/fbcon

这里需要区分 fbconfb0

1
2
3
4
5
tty1

fbcon framebuffer console,负责把 virtual console 显示到 framebuffer

fb0 framebuffer device,实际可以通过 /dev/fb0 访问

前面已经检查过 fbcon,没有找到可用的 blank sysfs 接口

接下来准备测试的是 framebuffer 的 FBIOBLANK ioctl,因此需要继续检查真正的 framebuffer device,也就是 fb0

1
2
3
4
5
6
7
# 查看 fb0 由谁提供
$ cat /sys/class/graphics/fb0/name
i915drmfb # Intel i915 DRM 提供的 framebuffer

# 确认对应的 framebuffer 设备节点存在
$ ls -l /dev/fb0
crw-rw---- 1 root video 29, 0 ... /dev/fb0

因此当前显示链路可以简单理解为:

1
tty1 → fbcon → fb0 (i915drmfb) → i915/DRM → 内置屏幕

/dev/fb0 存在,就可以继续尝试直接向 framebuffer 发送 FBIOBLANK ioctl


5. 为什么 --blank force 不能当成“立即执行一次普通 blank”

这里也是整个问题里最容易误解的地方

setterm 的帮助是:

1
2
$ setterm --help | grep -A3 -- '--blank'
--blank[=0-60|force|poke] set time of inactivity before screen blanks

乍一看很容易把:

1
$ setterm --blank 1

和:

1
$ setterm --blank force

理解成:

1
2
--blank 1        等一分钟再 blank
--blank force 不等一分钟,现在立即 blank

实际上两者并不是简单的“延迟”和“立即”的区别

正常 timeout blank 的行为是:

1
2
3
4
5
6
7
8
9
console 空闲

达到 timeout

正常 blank

键盘产生输入

自动 unblank

--blank force 使用的是强制 blank 行为

它不是把 blank timer “快进到现在”,而是要求 console 强制进入 blank 状态

所以实际行为变成:

1
2
3
4
5
6
7
setterm --blank force

强制 blank

键盘产生输入

仍保持 blank

需要显式:

1
$ setterm --blank poke

才能解除

这就解释了之前观察到的差异:

1
2
3
4
5
6
7
8
9
10
11
12
setterm --blank 1

超时自动熄屏

键盘可以唤醒


setterm --blank force

立即强制熄屏

键盘不能自动唤醒

因此 --blank force 并不适合“立即熄屏,但之后允许键盘唤醒”这个需求


6. 使用 FBIOBLANK 测试正常 framebuffer blank

既然系统存在:

1
/dev/fb0

并且:

1
fb0 = i915drmfb

就可以直接向 framebuffer 发 FBIOBLANK ioctl:

1
2
3
4
$ sudo python3 -c 'import os,fcntl; fd=os.open("/dev/fb0",os.O_RDWR); fcntl.ioctl(fd,0x4611,1); os.close(fd)'
# os.open("/dev/fb0", os.O_RDWR) 打开 framebuffer
# 0x4611 FBIOBLANK
# 1 FB_BLANK_NORMAL

测试结果:

1
2
3
4
5
6
7
执行 FBIOBLANK

立即黑屏

按 MacBook 键盘

自动亮屏

正好满足目标

这也进一步证明了之前的问题不是:

  • i915 无法恢复屏幕
  • T2 键盘无法唤醒
  • framebuffer 有问题

而是 setterm --blank force 本身的语义不符合这个需求


结论

对于没有桌面环境、直接运行 Linux virtual console 的机器,可以把熄屏需求分成两种

空闲一段时间后自动熄屏

使用:

1
2
3
$ sudo sh -c 'TERM=linux setterm --blank 1 < /dev/tty1 > /dev/tty1'
# --blank 1 空闲 1 分钟后自动 blank
# /dev/tty1 指定本机 virtual console

效果:

1
空闲 1 分钟 → 黑屏 → 本地键盘输入 → 自动亮屏

通过 systemd oneshot service,可以让它每次开机自动生效

立即熄屏,同时允许键盘唤醒

在这台使用 i915drmfb 的机器上:

1
2
3
$ sudo python3 -c 'import os,fcntl; fd=os.open("/dev/fb0",os.O_RDWR); fcntl.ioctl(fd,0x4611,1); os.close(fd)'
# 0x4611 FBIOBLANK
# 1 FB_BLANK_NORMAL

效果:

1
执行命令 → 立即黑屏 → 本地键盘输入 → 自动亮屏

不要把下面这条命令当成“立即执行一次正常 blank”:

1
2
$ sudo sh -c 'TERM=linux setterm --blank force < /dev/tty1 > /dev/tty1'
# force 强制 blank,普通键盘输入不会按 timeout blank 的方式自动解除

如果需要从 SSH 显式点亮屏幕,则可以使用:

1
2
$ sudo sh -c 'TERM=linux setterm --blank poke < /dev/tty1 > /dev/tty1'
# poke 显式解除 console blank

be slow to promise and quick to perform.