账号管理器可以在没有登录时继续存在,已启用服务仍有各自生命周期。

Linger 延长 systemd 用户管理器在登录之外的寿命

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 已开启、会话列表为空。但已经安装的工作单元仍然非活动。这证明让管理器保持可用,与请求某个应用运行,是不同操作;即使文件已经出现在正确配置目录中,也还需要符合实际契约的激活入口。 ...

四月 20, 2027 · 2 分钟 · XB Field Notes
两个登录会话共享一个账号管理器,以及由它独立监管的用户服务。

同一账号的会话共享 systemd 用户管理器

systemd 用户管理器代表一个账号运行服务。同一账号的多个登录会话通常共享这个管理器,而不是每个 shell 都拥有一套私有服务世界。用户单元可以放在 ~/.config/systemd/user,通过 systemctl --user 操作对应管理器。这提供的是以账号权限执行的生命周期管理,并不自动隔离所有共享系统资源。使用它之前,需要同时理解服务属于谁,以及管理器在什么条件下继续存在。 坏掉的样子 关闭一个终端,不一定停止用户服务,因为另一个登录会话可能仍然存在,管理器寿命也受到 linger 和停止延迟配置影响。即使最后一个会话结束,立即终止也不是普遍规则。必须跟随某一次图形会话结束的应用,需要明确的生命周期关系,不能假定任何用户单元天然具有这个范围。账号级后台任务与某个窗口的寿命,是不同需求。 选错管理器也是常见故障。普通 systemctl 操作系统管理器,systemctl --user 则需要连接正确用户的管理器及其运行时通信端点。通过另一身份执行命令,或者所在环境没有完成会话准备,都可能导致连接失败。仅仅设置一个环境变量,不会创建管理器,也不会授予控制其他账号的权限;诊断时应先确认执行身份与目标总线是否一致。 用户服务也不会自动继承发出控制命令的那个终端环境。所需路径、配置和图形会话变量,应通过有意选择的机制提供。使用普通账号会减少权限,但该账号仍可以按既有权限访问共享可写文件、使用网络套接字并消耗资源。因此“没有 root”并不等于“所有影响都绝对局限于自己的文件”,还要检查真实权限和资源限制。 快速验证 在具备 systemd、logind、PAM 集成、用户 D-Bus 会话与账号管理工具的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。记录环境为 Debian 12、systemd 252.39。脚本创建临时账号、两个真实 PAM 会话和一个用户服务,不修改任何已有人员的单元或登录状态: from pathlib import Path from tempfile import TemporaryDirectory import os,pwd,subprocess,time,uuid name='xbcatm-user-'+uuid.uuid4().hex[:8];sessions=[name+'-a.service',name+'-b.service'];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) for session in sessions: run('systemd-run','--quiet','--unit='+session,'--property=Type=simple', '--property=User='+name,'--property=PAMName=login','/usr/bin/sleep','60') wait_for(lambda:Path(f'/run/user/{uid}/bus').exists()) wait_for(lambda:len(run('loginctl','show-user',name,'-p','Sessions','--value').split())==2) assert run('loginctl','show-user',name,'-p','Linger','--value')=='no' initial=manager_value('MainPID');assert initial!='0' userctl('enable','--now',unit);wait_for(lambda:(home/'worker-pid').exists()) worker_pid=(home/'worker-pid').read_text() assert userctl('show',unit,'-p','MainPID','--value')==worker_pid run('systemctl','stop',sessions[0]) wait_for(lambda:len(run('loginctl','show-user',name,'-p','Sessions','--value').split())==1) assert manager_value('MainPID')==initial and userctl('show',unit,'-p','MainPID','--value')==worker_pid print('two PAM sessions share one user manager; closing one leaves its service unchanged') run('systemctl','stop',sessions[1]) wait_for(lambda:manager_value('ActiveState')=='inactive',seconds=20) assert not Path('/proc/'+worker_pid).exists() print('after the final session and configured stop delay, non-lingering manager and worker stop') print('controllers use setpriv and the actual user bus; they do not create extra PAM sessions') finally: for session in sessions: if run('systemctl','show',session,'-p','LoadState','--value')!='not-found':run('systemctl','stop',session) run('systemctl','stop',manager,f'user-runtime-dir@{uid}.service') run('userdel',name) 两个临时会话启动器使用相同账号,并设置 PAMName=login。脚本等待 logind 报告两个会话、且用户总线可用后,才继续控制服务。启动器使用 simple 类型,因此命令返回不会被当成 PAM 已完成的证据;会话数量和总线状态提供了明确的观察边界,避免把尚在建立的登录环境当成已经可用。 ...

一月 26, 2027 · 2 分钟 · XB Field Notes