系统抑制锁
ssk-wh Lv4

Systemd的183或更新的版本包含了一个逻辑来阻止系统进入关闭和睡眠状态。由systemd-logind.daemon进行了实现。

场景

1.CD 刻录应用程序希望确保系统在刻录过程中不会关闭或挂起

2.包管理员希望确保在包升级过程中系统不会关闭

3.办公套件希望在系统挂起之前得到通知,以便将所有数据保存到磁盘,并延迟挂起逻辑,直到所有数据写入

4.Web 浏览器希望在系统休眠之前得到通知,以便释放其缓存以最小化需要虚拟化的内存量

5.屏幕锁定工具想要在挂起之前立即调出屏幕锁定,并延迟挂起直到挂起完成。想要使用禁止逻辑的应用程序应通过登录 D-Bus API获取抑制锁。

类型

抑制锁共有7中类型,可以采用其一或者他们的组合:

类型 说明
sleep 禁止(非特权)用户请求的系统挂起和休眠
shutdown 禁止(非特权)用户请求的高级系统关闭和重启
idle 禁止系统进入空闲模式,可能导致系统自动挂起或关闭,具体取决于配置
handle-power-key 禁止系统电源硬件密钥的低级(即登录内部)处理,允许(可能是非特权的)外部代码来处理事件。
handle-suspend-key 禁止对系统硬件挂起键进行低级处理。
handle-hibernate-key 禁止对系统硬件休眠键进行低级处理。
handle-lid-switch 禁止对 systemd 硬件盖(例如,笔记本合盖开盖)开关进行低级处理。

支持两种不同的锁模式:

模式 说明
block 完全禁止操作,直到锁被释放。如果使用这样的锁,操作将失败(但如果用户拥有必要的权限,则仍可能被覆盖)
delay 仅暂时禁止操作,直到释放锁或达到一定时间。 logind.conf(5) 中的 InhibitDelayMaxSec字段可以控制超时时长。 这旨在供需要在系统挂起之前同步执行操作但又不允许无限期地阻止挂起的应用程序使用。 此模式仅适用于睡眠和关机锁。

DBus

抑制器锁是通过调用logind Manager对象的 Inhibit() D-Bus方法获取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ gdbus introspect --system --dest org.freedesktop.login1 --object-path /org/freedesktop/login1
node /org/freedesktop/login1 {
interface org.freedesktop.login1.Manager {
methods:
Inhibit(in s what,
in s who,
in s why,
in s mode,
out h fd);
ListInhibitors(out a(ssssuu) inhibitors);
...
signals:
PrepareForShutdown(b active);
PrepareForSleep(b active);
...
properties:
readonly s BlockInhibited = '';
readonly s DelayInhibited = '';
readonly t InhibitDelayMaxUSec = 5000000;
readonly b PreparingForShutdown = false;
readonly b PreparingForSleep = false;
...
};
...
};

Inhibit

Inhibit方法是获取锁所需的唯一 API。它需要四个参数:

参数 说明
What 冒号分隔的锁类型列表,即shutdown, sleep, idle, handle-power-key, handle-suspend-key, handle-hibernate-key, handle-lid-switch. 例如: “shutdown:idle”
Who 是一个人类可读的描述性字符串,说明谁在拿锁。示例:“包更新程序”
Why 是一个人类可读的描述性字符串,说明为什么要锁定。示例:“包更新正在进行中”
Mode 模式是block或delay中的一个,见上文描述。示例:“block”

调用Inhibit后会返回一个值,这是一个封装锁的文件描述符。 一旦文件描述符(及其所有副本)关闭,锁就会自动释放。 如果客户端在获取锁时死亡,内核会自动关闭文件描述符,以便自动释放锁。 采用这种方式的延迟锁应该在收到 PrepareForShutdown(true)(见下文)时尽快释放。
(当然只有在执行了应用程序首先想要延迟操作的操作之后)。

ListInhibitors

ListInhibitors列出所有当前活动的抑制剂锁。它返回一个结构数组,每个结构由上面的 What、Who、Why、Mode 以及请求锁的进程的 PID 和 UID 组成。

PrepareForShutdown() 和 PrepareForSleep() 信号在请求系统挂起或关闭并即将执行时发出,以及在挂起/关闭完成(或失败)之后发出。 这些信号带有一个布尔参数。 如果为 True,则已请求关闭/睡眠,并开始准备阶段,如果为 False,则操作已完成(或失败)。 如果为 True,这应该用作应用程序在挂起/关闭之前快速执行它们想要执行的操作的指示,然后释放所采取的任何延迟锁定。 如果为 False,则挂起/关闭操作结束,无论是成功还是失败(当然,如果关闭请求成功,则永远不会发送此信号)。 带有 False 的信号通常只有在系统从挂起状态恢复后才会被传送,带有 True 的信号也可能是这样,例如当一开始没有延迟锁定时,系统挂起因此没有任何延迟地执行。 带有 False 的信号通常是应用程序请求新延迟锁定的信号,以便同步通知下一个挂起/关闭周期。

请注意,在不使用延迟锁的情况下观看 PrepareForShutdown(true)?/PrepareForSleep(true) 是活泼的,不应该这样做,因为应用程序可能希望在此信号上执行的任何代码在挂起/关闭周期之前实际上可能不会完成执行。

再次重申:如果您观察 PrepareForSuspend(true),那么您真的应该先进行延迟锁定。 PrepareForShutdown(false) 可以由希望收到系统恢复事件通知的应用程序订阅。

请注意,这将仅针对通过登录完成的挂起/恢复周期发送,即通常仅针对高级用户引发的挂起周期,而不是自动的、低级内核引发的暂停周期,这些暂停周期可能存在于某些具有更强大功能的设备上 管理。

BlockInhibited & DelayInhibited

BlockInhibited 和 DelayInhibited 属性对当前使用的锁类型进行编码。

这些字段是以冒号分隔的shutdown, sleep, idle, handle-power-key, handle-suspend-key, handle-hibernate-key, handle-lid-switch 列表。

该列表基本上是特定模式的所有当前活动锁的 What 字段的联合。

InhibitDelayMaxUSec

InhibitDelayMaxUSec 包含在 logind.conf(TODO) 中配置的延迟超时值。

PreparingForShutdown & PreparingForSleep

The PreparingForShutdown and PreparingForSleep boolean properties are true between the two PrepareForShutdown() resp PrepareForSleep() signals that are sent out.
需要注意的是,这两个属性的变化并不会触发PropertyChanged信号。

使用

Block

以下是需要阻塞锁的应用程序的基本方案,例如包管理器或 CD 刻录应用程序:

1.获取锁

2.做你不想被系统睡眠或关机打断的工作

3.释放锁

示例伪代码:

1
2
3
4
5
fd = Inhibit("shutdown:idle", "Package Manager", "Upgrade in progress...", "block");
/* ...
do your work
... */
close(fd);

Delay

以下是需要延迟锁的应用程序(例如 Web 浏览器或办公套件)的基本方案:

1.当你打开一个文件时,采取延迟锁定

2.一旦看到 PrepareForSleep(true),保存数据,然后释放锁

3.一看到PrepareForSleep(false),就重新拿delay lock,和之前一样继续。

示例伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int fd = -1;

takeLock() {
if (fd >= 0)
return;

fd = Inhibit("sleep", "Word Processor", "Save any unsaved data in time...", "delay");
}

onDocumentOpen(void) {
takeLock();
}

onPrepareForSleep(bool b) {
if (b) {
saveData();
if (fd >= 0) {
close(fd);
fd = -1;
}
} else
takeLock();

}

Key Handling

默认情况下,logind 将处理机器的电源和睡眠键,以及所有状态下的盖子开关。 这可确保此基本系统行为在所有情况下都能正常工作,包括文本控制台和所有图形环境。

然而,一些 DE 可能想要自己处理这些键,例如为了在执行相关操作之前显示一个漂亮的对话框,或者在某些条件下简单地禁用该操作。

对于这些情况,可以使用 handle-power-key、handle-suspend-key、handle-hibernate-key 和 handle-lid-switch 类型的抑制器锁。

获取时,这些锁只是禁用键的低级处理,它们对使用硬件键以外的其他机制执行的系统挂起/休眠/关机没有影响(例如用户在 shell 中键入“systemctl suspend”)。

打算自己处理这些密钥的 DE 应该简单地在登录时获取锁,并在注销时释放它们; 或者,在某些情况下仅暂时锁定此锁可能是有意义的(例如,仅在插入第二个显示器时才锁定盖子开关,以支持人们在连接大屏幕时关闭笔记本电脑的常见设置) .

这些锁需要以“block”模式获取,不支持“delay”。

如果 DE 想要确保在系统进入挂起状态之前屏幕上显示最终恢复的锁定屏幕,它应该通过挂起延迟抑制块(见上文)来实现。

Miscellanea

获取抑制锁是一项特权操作。取决于systemd安装的policy(org.freedesktop.login1.policy)策略中下列action的配置:

org.freedesktop.login1.inhibit-block-shutdown

org.freedesktop.login1.inhibit-delay-shutdown

org.freedesktop.login1.inhibit-block-sleep

org.freedesktop.login1.inhibit-delay-sleep

org.freedesktop.login1.inhibit-block-idle

org.freedesktop.login1.inhibit-handle-power-key

org.freedesktop.login1.inhibit-handle-suspend-key

org.freedesktop.login1.inhibit-handle-hibernate-key

org.freedesktop.login1.inhibit-handle-lid-switch

一般来说,应该假设延迟锁比阻塞锁更容易获得,只是因为它们的影响要小得多。请注意,Inhibit() 的策略检查从不交互。
抑制器锁不应被滥用。例如,在没有充分理由的情况下使用空闲阻塞锁可能会导致移动设备永远不会自动挂起。这可能对电池非常不利。

如果应用程序发现锁被拒绝,它不应该认为这是一个错误,没有保护锁的情况下继续其操作即可

systemd-inhibit工具可用于从命令行获取锁或列出活动锁。

请注意,gnome-session 还提供了一个 inhibitor API,它与 systemd 的非常相似。 在内部,对 gnome-session 接口的锁定将转发给 logind,因此这两个 API 都受支持。 虽然两者都提供相似的功能,但它们在某些方面确实有所不同。 出于显而易见的原因,gnome-session 可以提供登录所缺少的注销锁定和屏幕保护程序避免锁定。 logind 的 API OTOH 除了像 GNOME 一样支持块锁外,还支持延迟锁。 此外,logind 可用于系统组件,并集中所有用户的锁,而不仅仅是特定用户的锁。

一般来说:如果有疑问,最好坚持使用 GNOME 锁,除非有充分的理由直接使用 logind API。 但是,当要枚举锁时,最好使用 logind API,因为它们还包括系统服务和其他用户获取的锁。

附录

https://www.freedesktop.org/wiki/Software/systemd/inhibit/
https://www.freedesktop.org/wiki/Software/systemd/logind/

 Comments