本文面向已经会使用 Linux 命令行、但还没有系统理解 systemd 的读者。目标是读完后能独立创建一个长期运行服务、一个一次性任务服务,以及一个替代 cron 的 systemd timer。
这篇文章也服务于后续 Linux 安全排查:攻击者可能把后门、挖矿程序、反连脚本伪装成 systemd service 或 timer 持久化运行。因此除了“怎么创建服务”,还要掌握“怎么枚举所有服务、怎么查看某个服务到底执行什么、怎么找定时触发的可疑任务”。
1. systemd 是什么
在多数现代 Linux 发行版中,
systemd 是系统和服务管理器。它在系统启动时作为 PID 1 运行,负责把用户空间服务拉起来,并持续跟踪、管理这些服务。相比传统 init 脚本,systemd 的核心抽象是 unit:服务、定时器、socket、挂载点、target 等都被描述成 unit 文件。官方手册把 unit 文件定义为一种纯文本、INI 风格的配置文件,用来描述 service、socket、device、mount、timer 等对象;通用配置写在
[Unit] 和 [Install],服务自己的配置写在 [Service],定时器自己的配置写在 [Timer]。参考:systemd.unit(5)、systemd.service(5)、systemd.timer(5)。可以先看 PID 1:
查看 systemd 版本:
2. unit 和 service 的关系
service 是 unit 的一种。一个 nginx.service、ssh.service、docker.service 本质上都是 unit 文件,只是后缀是 .service,因此它们拥有 [Service] 段。常见 unit 类型包括:
.service:服务进程,最常见。
.timer:定时触发另一个 unit,通常触发同名.service。
.socket:socket 激活,收到连接后再拉起服务。
.target:一组 unit 的同步点,类似“运行级别”的现代替代。
.mount/.automount:挂载点。
.path:监听文件路径变化后触发 unit。
列出当前已加载的 unit:
列出已安装的 service unit 文件:
安全排查时,不要只看 running 服务,也要看所有 service 的启用状态:
列出所有已加载服务,包括 inactive、failed:
只看失败服务:
排查时这些异常值得重点看:名称像系统服务但不属于发行版包、
enabled 但业务上没人知道、描述含糊如 Update Helper、服务文件位于 /etc/systemd/system/ 且创建时间很新、ExecStart= 指向 /tmp、/dev/shm、隐藏目录、用户家目录或随机命名二进制。3. 服务文件放在哪里
systemd 会从多个目录加载 unit。不同发行版略有差异,但最重要的路径可以这样理解:
目录 | 用途 | 优先级与建议 |
/etc/systemd/system/ | 管理员自己创建的服务、覆盖发行版默认服务的配置 | 优先级高。自定义服务通常放这里 |
/run/systemd/system/ | 运行时临时 unit | 重启后消失 |
/usr/lib/systemd/system/ 或 /lib/systemd/system/ | 软件包安装的 vendor unit | 不要直接改,升级可能覆盖 |
~/.config/systemd/user/ | 当前用户级服务 | 配合 systemctl --user 使用 |
查看某个服务实际来自哪里:
查看 systemd 实际识别到的片段路径:
如果要改软件包自带服务,不推荐直接编辑
/usr/lib/systemd/system/xxx.service。推荐用 drop-in:示例输出通常是编辑器界面;保存后可验证:
4. 一个 service 文件长什么样
最小服务文件一般由三段组成:
三段含义:
[Unit]:写元信息和依赖关系。After=只表达启动顺序,Wants=/Requires=才表达拉起依赖。
[Service]:写服务如何运行。ExecStart=是主命令,User=指定运行用户,Restart=指定异常退出后的重启策略。
[Install]:写 enable 时如何挂到启动目标上。WantedBy=multi-user.target的意思是启用后,系统进入多用户模式时拉起它。
Type= 很关键:simple:默认类型,systemd fork 出进程后就认为启动成功。
exec:等到execve()成功后才认为启动成功,通常比simple更能暴露“命令不存在/用户不存在”等启动错误。
forking:适合老式会自我 daemonize 的程序,需要配合PIDFile=更可靠。
oneshot:一次性任务,执行完就结束,常和 timer 搭配。
notify:服务主动通过 sd_notify 告诉 systemd 自己 ready。
5. 创建一个长期运行服务
假设我们要把
/opt/qetx-demo/app.py 作为 HTTP 服务运行。先准备目录和脚本:创建专用用户:
写 unit 文件:
重新加载 systemd 配置:
daemon-reload 成功时通常没有输出,但它非常重要:systemd 不会因为你新建或修改 unit 文件就自动重读配置。官方 FAQ 也提到自动监听 unit 文件变化存在竞态问题,所以需要显式 reload。检查 unit 语法:
启动服务:
查看状态:
设为开机自启:
也可以启动并启用一步完成:
验证是否 enabled:
验证是否 active:
查看日志:
停止、重启、禁用:
6. systemctl 常用命令速查
命令 | 作用 | 典型输出 |
systemctl start qetx-demo | 立即启动服务 | 成功通常无输出,退出码 0 |
systemctl stop qetx-demo | 停止服务 | 成功通常无输出 |
systemctl restart qetx-demo | 重启服务 | 成功通常无输出 |
systemctl reload nginx | 让支持 reload 的服务重载配置 | 成功通常无输出;不支持会报错 |
systemctl status qetx-demo | 查看运行状态和最近日志 | Active: active (running) |
systemctl enable qetx-demo | 设置开机自启 | 创建 wants 目录下的符号链接 |
systemctl disable qetx-demo | 取消开机自启 | 删除 enable 创建的符号链接 |
systemctl daemon-reload | 重读 unit 文件 | 成功通常无输出 |
systemctl cat qetx-demo | 查看最终 unit 文件内容 | 显示 unit 文件和 drop-in |
systemctl edit qetx-demo | 创建 override drop-in | 打开编辑器 |
systemctl list-units --type=service --all | 查询当前所有已加载服务 | 显示 active、inactive、failed 服务 |
systemctl list-unit-files --type=service | 查询所有已安装 service 文件 | 显示 enabled、disabled、static 等状态 |
systemctl --failed --type=service | 查询失败服务 | 显示 failed 服务列表 |
systemctl show qetx-demo -p FragmentPath -p ExecStart -p User | 查看服务文件路径、启动命令、运行用户 | FragmentPath=/etc/systemd/system/qetx-demo.service |
systemctl list-timers --all | 查询所有 systemd 定时器 | 显示 NEXT、LAST、UNIT、ACTIVATES |
enable 和 start 要分清:start 是现在启动,enable 是设置未来启动。systemctl 手册也明确说明二者是正交关系:一个服务可以已启动但未启用,也可以已启用但当前没启动。参考:systemctl(1)。7. 定时服务:timer + service
systemd 的定时任务由
.timer unit 实现。它不像 cron 那样直接写一行命令,而是通常由两个文件组成:xxx.service:真正要执行的任务。
xxx.timer:什么时候触发这个任务。
默认规则是:
foo.timer 会触发同名的 foo.service。也可以在 [Timer] 中用 Unit=bar.service 指定要触发别的 unit。官方 timer 手册说明:每个 timer 文件都需要有一个匹配的 unit;默认激活同名 service。参考:systemd.timer(5)。8. 创建一个定时备份任务
先写脚本:
写一次性 service:
注意这里没有
[Install]。因为这个 service 不需要自己开机启动,它由 timer 触发。写 timer:
重载并启用 timer:
查看 timer:
手动触发一次 service 测试:
查看 timer 细节:
9. timer 的原理
timer 的工作方式可以拆成五步:
- systemd 读取
.timerunit,并根据[Timer]段计算下一次触发时间。
- timer 到点后,systemd 激活对应的 unit,通常是同名
.service。
- 如果被触发的 service 已经处于 active 状态,timer 不会再启动一个新实例,而是保持原样。这一点在官方 timer 手册中有明确说明。
- service 运行、退出、写日志,状态由 systemd 记录到 journal。
- timer 计算下一次触发时间,继续等待。
核心字段:
OnCalendar=:日历时间,类似 cron,适合“每天 03:30”“每周一”等。
OnBootSec=:开机后多久触发。
OnUnitActiveSec=:被触发 unit 上次激活后多久再次触发。
OnUnitInactiveSec=:被触发 unit 上次变为 inactive 后多久再次触发。
Persistent=true:机器关机或 timer 未运行时错过了触发时间,下次 timer 启动后补跑一次。
AccuracySec=:允许 systemd 在一个时间窗口内合并唤醒,默认可能不是秒级精准;需要更准可调小。
RandomizedDelaySec=:给触发时间加随机延迟,适合避免大量机器同时打到同一个服务。
Unit=:指定 timer 触发哪个 unit。
示例:每 10 分钟跑一次:
启用后查看:
示例:每周一凌晨 4 点,并随机延迟 30 分钟:
验证日历表达式:
10. timer 和 cron 的区别
维度 | cron | systemd timer |
任务定义 | 一行时间 + 命令 | .timer 负责时间,.service 负责执行 |
日志 | 依赖 cron/mail/syslog 配置 | 天然进入 journal,可按 unit 查询 |
依赖关系 | 较弱 | 可用 After=、Wants=、Requires= 表达 |
错过任务 | 默认不补跑 | Persistent=true 可补跑 |
资源与安全 | 需要额外包装 | 可直接用 User=、MemoryMax=、PrivateTmp= 等 |
如果只是个人 crontab 里一条简单命令,cron 仍然很方便;如果任务是系统服务的一部分,需要日志、依赖、失败状态、权限隔离和可观测性,systemd timer 通常更适合。
11. 面向安全排查的服务和定时器查询
在入侵排查里,systemd 需要重点关注两个面:服务持久化 和 定时任务持久化。攻击者可能创建一个看似正常的 service,让木马开机自启;也可能创建 timer,每隔几分钟拉起脚本、下载 payload 或恢复被删除的进程。
11.1 查询当前所有服务
查询所有已加载服务:
查询所有 service 文件以及是否自启:
只看 enabled 服务,适合快速找开机自启项:
查看失败服务,失败的后门服务、残留服务也可能暴露痕迹:
11.2 关闭服务开机自启动
确认某个服务当前是否开机自启动:
只取消开机自启动,但不停止当前正在运行的进程:
再次确认:
如果安全排查中已经确认服务可疑,通常要同时停止当前进程并取消开机自启动:
验证服务已经停止且不再自启:
如果目标是 timer 定时任务,应该关闭
.timer,而不是只关闭它触发的 .service。因为 timer 下次到点后仍可能再次拉起 service:如果已经确认是恶意持久化,停止和禁用只是第一步。还需要保留证据后处理 unit 文件、可执行文件、日志、网络连接、父子进程和相关账号。不要在没有取证需求判断前直接删除所有痕迹,否则可能影响后续溯源。
11.3 查看某一个具体服务
先看状态和最近日志:
看 unit 文件真实内容,尤其关注
ExecStart=、User=、WorkingDirectory=、Restart=:用
show 抽取关键字段,适合脚本化排查:根据 MainPID 继续看进程、网络连接和文件路径:
查看该服务日志:
11.4 查询所有定时器
systemctl list-timers --all 是排查 systemd 定时任务的核心命令。默认不加 --all 时只显示即将触发或活跃 timer;加上 --all 可以把 inactive、已经错过、未启用但已加载的 timer 也列出来。只查某一个 timer:
查看 timer 文件内容:
抽取 timer 关键字段:
11.5 直接从目录层面查可疑 unit
按时间排序查看管理员目录中新建或修改的 unit:
搜索高危路径或可疑命令:
查看 enable 创建的符号链接,理解服务是如何挂到启动目标上的:
11.6 排查时的判断清单
- 服务名是否伪装成系统组件,例如
system-update.service、dbus-helper.service、kworker.service。
Description=是否模糊但无法对应业务。
FragmentPath=是否在/etc/systemd/system/,且修改时间接近入侵时间。
ExecStart=是否指向/tmp、/var/tmp、/dev/shm、隐藏目录、用户家目录、.cache。
- 是否使用
bash -c、curl | bash、wget -O-、nc、socat、python -c等高风险启动方式。
- 是否
Restart=always,被杀后会自动拉起。
- 是否存在同名
.timer反复触发可疑.service。
- 运行用户是否异常,尤其是普通业务不需要 root 却用 root 运行。
- 日志是否被清空或只有极少输出,unit 文件是否刻意把输出重定向到
/dev/null。
12. 排错思路
查看 unit 是否加载成功:
看详细日志:
改完 unit 后忘了 reload 的典型提示:
重置 failed 状态:
检查依赖链:
13. 服务安全加固的常用字段
很多服务不应该直接用 root 跑。systemd 可以在 unit 里直接做基础隔离:
验证最终属性:
这些字段属于执行环境配置,更多可以查 systemd.exec(5)。
14. 一个推荐实践清单
- 自定义系统服务放
/etc/systemd/system/。
- 软件包自带服务不要直接改,用
systemctl edit xxx.service创建 drop-in。
- 修改或新增 unit 后运行
sudo systemctl daemon-reload。
- 新服务先
systemd-analyze verify,再systemctl start。
- 长期运行服务优先考虑
Type=exec和合适的Restart=。
- 一次性任务用
Type=oneshot,再交给.timer定时触发。
- timer 启用的是
.timer,不是.service。
- 需要错过后补跑就加
Persistent=true。
- 排错优先看
systemctl status和journalctl -u。
- 生产服务尽量指定专用用户,并开启必要的 sandbox 选项。
15. 参考资料
- 作者:NotionNext
- 链接:http://qetx.top/article/linux-systemd-service-and-timer-guide
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章






