Linger 改变的是一个账号的 systemd 用户管理器在什么时候保持可用。启用后,logind 可以在开机时启动该管理器,并在该账号登录会话结束后继续保留它,从而支持普通账号拥有的无人值守工作。这个标志控制管理器寿命,各个服务仍然遵守自己的激活、失败、停止和安装规则。账号可以没有登录会话,同时有管理器运行,但这不等于所有应用都已经启动。

坏掉的样子

开启 linger 不会自动启用账号配置目录里的每一个单元文件。服务仍需要通过启用关系、依赖、定时器、套接字或其他明确路径受到激活。因此管理器可能完全正常,目标应用却保持非活动。应同时检查两者,而不能把一个 linger 标志理解成当前所有服务的运行清单,更不能仅凭它存在就认定某个后台端口已经提供业务响应。

进程不会跨过整台机器重启继续存活。Linger 可以让新的用户管理器在开机时启动,已启用单元再创建新的进程,原来的内存状态已经消失。加密家目录或较晚挂载的路径,也可能让配置和应用数据在管理器启动时不可用。需要保留的状态应持久化,开机依赖应明确安排,不能靠“会跨重启”这种模糊说法跳过实际恢复过程。

Linger 也不是让崩溃应用无限重启的指令,不会自动把任意 shell 后台任务纳入服务管理。重启策略、资源限制和 logind 对会话进程的处理,分别属于不同控制。稳定的后台角色需要真实用户单元和可观察结果,不能只依赖登出前启动过一个命令,再把账号管理器继续存在当成那个命令必然受控运行。

快速验证

在具备 systemd、logind、用户 D-Bus 支持与账号管理工具的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。实测环境为 Debian 12、systemd 252.39。它创建临时账号和家目录,只为该账号开启 linger,最后回收本次改动,全程不为这个账号建立登录会话:

from pathlib import Path
from tempfile import TemporaryDirectory
import os,pwd,subprocess,time,uuid

name='xbcatm-linger-'+uuid.uuid4().hex[:8];unit='field-worker.service'
def run(*args):
    result=subprocess.run(args,capture_output=True,text=True,timeout=20)
    assert result.returncode==0,(args,result.stderr)
    return result.stdout.strip()
def wait_for(check,seconds=5):
    deadline=time.monotonic()+seconds
    while not check():
        assert time.monotonic()<deadline,'state transition timed out';time.sleep(0.05)
with TemporaryDirectory(prefix=name+'-',dir='/run') as directory:
    root=Path(directory);root.chmod(0o755);home=root/'home';home.mkdir()
    run('useradd','--no-create-home','--user-group','--home-dir',str(home),'--shell','/bin/sh',name)
    account=pwd.getpwnam(name);uid=account.pw_uid;gid=account.pw_gid;manager=f'user@{uid}.service'
    def userctl(*args):
        return run('setpriv','--reuid='+str(uid),'--regid='+str(gid),'--init-groups',
                   'env',f'XDG_RUNTIME_DIR=/run/user/{uid}',f'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus',
                   'systemctl','--user',*args)
    def manager_value(key):return run('systemctl','show',manager,'-p',key,'--value')
    try:
        units=home/'.config/systemd/user';units.mkdir(parents=True)
        (home/'worker.py').write_text("from pathlib import Path\nimport os,time\np=Path(__file__).with_name('worker-pid');t=p.with_suffix('.tmp');t.write_text(str(os.getpid()));t.replace(p)\ntime.sleep(60)\n")
        (units/unit).write_text('[Service]\nType=exec\nExecStart=/usr/bin/python3 %h/worker.py\n[Install]\nWantedBy=default.target\n')
        for path in [home,*home.rglob('*')]:os.chown(path,uid,gid)
        run('loginctl','enable-linger',name)
        wait_for(lambda:Path(f'/run/user/{uid}/bus').exists())
        assert run('loginctl','show-user',name,'-p','Linger','--value')=='yes'
        assert run('loginctl','show-user',name,'-p','Sessions','--value')==''
        assert manager_value('ActiveState')=='active'
        assert userctl('show',unit,'-p','ActiveState','--value')=='inactive'
        print('linger starts a manager with zero login sessions; merely installing a user unit does not start it')
        userctl('enable','--now',unit);wait_for(lambda:(home/'worker-pid').exists())
        first=(home/'worker-pid').read_text();first_manager=manager_value('MainPID')
        run('systemctl','restart',manager)
        wait_for(lambda:Path(f'/run/user/{uid}/bus').exists() and (home/'worker-pid').read_text()!=first)
        assert manager_value('MainPID')!=first_manager
        assert userctl('show',unit,'-p','MainPID','--value')==(home/'worker-pid').read_text()
        assert run('loginctl','show-user',name,'-p','Sessions','--value')==''
        print('enabled user service starts as a new process after a real manager restart')
        userctl('disable',unit)
        assert userctl('show',unit,'-p','ActiveState','--value')=='active'
        print('disable removes future enablement but does not stop the current service')
        userctl('stop',unit);run('loginctl','disable-linger',name)
        wait_for(lambda:manager_value('ActiveState')=='inactive',seconds=20)
        print('with no sessions and linger disabled, user manager stops after policy delay')
    finally:
        run('loginctl','disable-linger',name)
        run('systemctl','stop',manager,f'user-runtime-dir@{uid}.service')
        run('userdel',name)

执行启用后,用户管理器变为活动,其总线端点出现,logind 报告 linger 已开启、会话列表为空。但已经安装的工作单元仍然非活动。这证明让管理器保持可用,与请求某个应用运行,是不同操作;即使文件已经出现在正确配置目录中,也还需要符合实际契约的激活入口。

随后脚本通过正确用户管理器启用并启动工作服务。安装段把它连接到用户默认 target,显式启动则开始当前执行,进程标记记录真正的工作者编号。控制器通过 setpriv 和用户运行时总线操作,不会悄悄建立登录会话。因此后续看到服务运行,不能归因于控制命令本身意外延长了某个会话。

重启系统层的 user@UID.service 后,账号管理器被替换,已启用的工作服务随后写出不同进程编号,新用户管理器也报告同一个新工作者,会话列表仍为空。这里验证的是根据保存的单元启用关系,在真实管理器重启后重新构建运行状态,而不是仅仅观察原进程多活了一段时间。新的进程身份也直接说明恢复与延续不是同一件事。

仅禁用工作服务、没有请求停止时,当前服务仍然活动。禁用移除的是后续启用链接,不是隐式终止命令。脚本检查这个差别,再显式停止工作者、关闭 linger;没有剩余会话时,账号管理器按停止策略回到非活动。因此停当前工作、改变未来激活和改变管理器寿命,需要分别表达和核对。

清理过程在需要时再次关闭 linger,停止实验管理器和运行时目录单元,删除临时账号与家目录。实验有意使用临时配置,没有重启整台机器。正文涉及开机的语义来自文档契约,实际观测到的进程替换来自管理器重启,不能冒称已经完成了机器重启验证。正式部署若要求开机可用,仍应在对应环境检验这个阶段。

有意管理无人值守账号

保持账号启用单元可审阅,及时移除不再需要的服务。登出后仍然可用的服务可能持续持有内存、打开的文件、凭据与网络监听器。普通用户权限限制了部分动作,但该身份已经获得的访问仍然存在,因此应按工作负载配置合适权限和受支持资源控制,不能把非 root 当成完全不影响共享系统的保证。

配置与凭据应通过无需交互 shell 的机制供应。终端中成功启动一次,不证明无人登录时具有相同环境;需要核对工作目录、输入路径和管理器重启后的访问。当开机可用是部署要求时,再执行真实开机验证。这样才能发现家目录挂载、环境变量或外部依赖只在交互登录后才成立的问题。

退役后台角色时,应区分禁用服务、停止当前执行、关闭 linger 和删除账号数据,其他会话或激活入口也可能继续影响结果。完成目标操作后,检查账号管理器及服务的真实状态。有效保证是一套写清楚的无人值守生命周期,具有明确激活与停止方式,而不是承诺某一个进程编号永远存活。

延伸阅读

来源