ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ubuntu开机自启脚本全攻略:systemd、cron与rc.local详解

Ubuntu开机自启脚本全攻略:systemd、cron与rc.local详解 最近有个朋友问我Ubuntu 上有个 run1.sh 脚本想让它开机自动跑一遍重启之后也自动执行该怎么弄他刚接触 Linux 没多久之前试过把脚本丢进/etc/rc.local结果重启后完全没反应整个人有点懵。这个问题表面上看只是“设置开机自启”这么简单但实际落到不同环境、不同脚本类型上解法真的不止一种。而且每一种方案都有自己的适用场景和坑选错了要么脚本不执行要么执行时机不对要么服务状态诡异到让你怀疑人生。这篇文章就把我从 systemd、cron 到 rc.local 这几条路都捋一遍把 run1.sh 这类脚本在 Ubuntu 上开机、重启自动运行的完整做法讲透同时把权限、环境变量、日志排查这些容易翻车的细节一起补上。1. run1.sh 这类脚本的自启需求先搞清楚该走哪条路搞 Linux 自启这件事最忌讳的就是一上来就搜“Ubuntu 开机启动脚本”然后照着某篇老教程把命令敲一遍。不同场景对应的自启机制完全不一样用错了等于白干。1.1 按运行阶段和用户环境拆分需求开机自启脚本按触发时机和运行环境大致可以分成三类。第一类是系统级服务型脚本比如内网穿透客户端、数据同步任务、监控采集脚本这类东西需要在系统启动、网络就绪之后就立刻跑起来而且最好是在独立的系统服务管理单元里运行。这种需求Ubuntu 上标准答案就是 systemd。从 15.04 开始Ubuntu 全面转向 systemd 作为 init 系统/etc/rc.local不再是默认启用的启动入口很多人直接改这个文件发现没用多半就是这个原因。第二类是用户登录型脚本比如你登录图形桌面之后要开的代理、要启动的应用这种不应该放在系统级服务里而是放在桌面环境的自启动目录下比如~/.config/autostart。如果放错地方容易出现“服务已经跑起来了但用户压根没登录”或者反过来“登录了却没触发”的尴尬。第三类是定时观察型脚本它不需要系统启动那么早执行只要在整机开机之后的某个时间点跑一次就行。这种情况可以用reboot的 cron 任务来搞定灵活度高写起来也简单。1.2 选型时先问自己三个问题我在处理这类需求时一般会快速过一遍这几个问题脚本要不要依赖网络如果需要就必须选network.target之后才执行的 systemd 服务或者用cron的rebootcron 启动时网络一般也已经起来了。脚本运行的用户是谁如果需要访问你的 SSH 密钥、桌面环境的加密钥匙串那就别放系统服务里否则环境变量和 keyring 的问题能坑死你。脚本执行时间多长是跑一下就退出还是会常驻后台这会决定 systemd 服务要用Typeoneshot还是TypesimpleExecStart。把这些问题的答案拿出来再去看具体方案心里就非常有底了。2. systemd 服务文件开机自启脚本的首选答案如果你的 run1.sh 是那种要在系统启动阶段跑、并且最好由 init 系统托管状态的脚本systemd 服务基本是最稳的选择。它自带崩溃检测、日志采集、依赖控制比裸写/etc/rc.local不知道高到哪里去。2.1 服务文件的结构与关键配置含义在/etc/systemd/system/目录下创建一个.service文件名字随意但建议跟脚本功能强关联比如run1.service。内容大概是这样的[Unit] DescriptionRun run1 script at startup Afternetwork.target [Service] Typeoneshot ExecStart/home/ubuntu/scripts/run1.sh RemainAfterExityes StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target这里面几个配置项值得展开讲讲。Typeoneshot是最关键的一项。如果你的 run1.sh 执行完就会退出它天然就是一个“一次性任务”不是守护进程。设为oneshot后systemd 会认为 ExecStart 启动的进程退出属于正常完成而不是“服务崩溃”。如果这里误设成Typesimple服务会处于“已经启动但执行完脚本又退出”的诡异状态用systemctl status查的时候会看到inactive (dead)或者反复重启非常迷惑。Afternetwork.target的意思不是“网络完全就绪后”而是“网络管理单元启动之后”。它保证脚本运行时网卡基本可用了但不保证外网一定能连上。如果你的 run1.sh 需要联网访问外部服务最好再加个Afternetwork-online.target和Wantsnetwork-online.target这样会更稳妥。RemainAfterExityes是给 oneshot 服务用的“装饰品”。加了它以后即使脚本里的所有进程都已经退出服务状态依然显示active而不是变成inactive。这在运维上非常友好不然每次查状态都看到一坨dead提示总是让人心里毛毛的。StandardOutputjournal让脚本的输出直接进 systemd 日志排查问题的时候可以用journalctl -u run1.service直接看比你自己搞个 log 文件再用 tail 命令追方便得多。2.2 文件创建到验证的完整流程先把服务文件写好然后依次执行这几个命令sudo nano /etc/systemd/system/run1.service sudo systemctl daemon-reload sudo systemctl enable run1.service sudo systemctl start run1.service这里每一步都有讲究daemon-reload是必须的。你新增或修改了 systemd 配置文件后不重载的话 systemd 根本不认识这个新服务直接 start 会报Failed to start run1.service: Unit run1.service not found。enable的作用是建立开机自启的软链接把服务挂到multi-user.target这个系统目标下。系统进入多用户模式也就是正常开机后的运行级别时会自动拉起这个服务。start则是立刻手动执行一次用来验证服务文件本身有没有写错。这一步在生产环境非常推荐先做确认没问题再重启整机验证效率高很多。验证是否已经成功挂上自启systemctl is-enabled run1.service systemctl status run1.service看到enabled和active (exited)基本就成了。提示生产环境里我一般会手动 start 一次确认脚本本身执行没问题、日志里没有报错再去做整机重启测试。不然一关机一开机发现问题再改来回折腾浪费时间。2.3 脚本自身权限与路径的硬性要求很多新手在这里会卡住服务创建了、也 enable 了脚本就放在/home/ubuntu/run1.sh但重启后就是不跑。查日志一看全是Permission denied。原因就一个systemd 不会帮你执行脚本它只是按 ExecStart 里写的命令去启动进程如果脚本没有可执行权限x 权限execve 就会失败。给整个脚本目录加上可执行权限chmod x /home/ubuntu/scripts/run1.sh另一个隐蔽的问题是相对路径。systemd 服务里 ExecStart 执行脚本时的当前目录通常不是你想象中的/home/ubuntu而是/之类的系统根目录。如果 run1.sh 内部用到了相对路径比如读同目录下的config.ini或者写入./result.txt那服务模式下执行的结果可能完全出乎意料。我自己的习惯是在脚本开头先cd $(dirname $0)切到脚本自身所在目录再执行后续逻辑。这样无论从哪个路径被调用脚本的工作目录都是统一的。如果脚本里引用了配置文件的绝对路径那就更简单全路径写死。3. cron 的 reboot 方式轻量灵活但有使用前提如果你的 run1.sh 不需要跟 systemd 服务绑定只想在系统重启后由普通用户身份自动跑一次cron 的reboot功能是最轻量的方案。3.1 配置与执行时机的差异编辑当前用户的 crontabcrontab -e在里面加一行reboot /home/ubuntu/scripts/run1.sh /var/log/run1.log 21保存退出后这个任务就挂上了。下次开机cron 守护进程启动时会扫描到这条reboot任务并执行一次。有几个细节你需要知道。reboot是 cron 的一个特殊字符串表示“每次系统启动后执行一次”注意是“每次启动”包括重启和开机。如果你的需求是“只在某天的某个时刻跑一次”应该用普通的时间表达式例如daily或者30 2 * * *别混淆概念。cron 任务默认环境变量极其精简。PATH很可能只有/usr/bin:/bin你脚本里如果用了python3、node这类命令而这些命令装在/usr/local/bincron 环境下会直接报command not found。所以在 run1.sh 里要么用绝对路径比如/usr/local/bin/python3要么在脚本开头自行 export PATH。另一个容易踩的坑是环境变量 HOME。cron 执行任务时HOME会被设置为该用户的家目录但这只在普通 crontab 里成立。如果脚本内部依赖某些 shell 全局变量、SSH agent 之类的用户态环境可能加载不到。建议在 crontab 里显式定义或者脚本内部自己处理reboot export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /home/ubuntu/scripts/run1.sh3.2 为什么有时候 cron 的 reboot 会失效我遇到过不止一次“明明配置了 reboot 但重启后没生效”的情况。最常见的原因是运行 crontab -e 的用户和 systemd 的 cron 服务启停时机不一致。如果你编辑的是某个普通用户的 crontab而这个用户在某些系统配置下并不会在开机时自动登录那么除了 cron 守护进程本身这个用户的任务是否执行其实取决于 cron 服务的状态。在 Ubuntu 上cron 服务默认是安装并开机自启的。可以检查一下systemctl is-enabled cron systemctl status cron如果输出不是enabled那就先开起来sudo systemctl enable --now cron另外/etc/cron.d/目录下的任务文件有严格的权限要求文件所有者必须是 root权限必须是 644 或 600且文件名不能包含点号。很多人自定义了/etc/cron.d/my-task结果就是不生效十有八九是权限或文件命名不规范。如果是普通用户的crontab则没有这种限制格式上相对宽松。3.3 对比cron 和 systemd 怎么选就“开机跑一次脚本”这件事cron 和 systemd 的差别主要在掌控力上。维度systemd 服务cron reboot启动时机由 target 依赖决定可精细控制cron 守护进程启动后触发日志采集journal 内置统一查询依赖你自己重定向到文件故障排查systemctl status / journalctl查日志文件 cron 邮件依赖管理After/Wants 显式声明无只能自己 wait 或 sleep用户环境默认精简可自定义精简需自己管 PATH适合场景服务化、需持久运行、依赖网络轻量脚本、一次性、用户级任务如果 run1.sh 只是类似“开机后清理某个临时目录”这种无状态任务cron 完全够用配置量也小。但如果脚本要等待某项系统服务就绪、要在特定服务失败时联动重启、或者需要持续运行那还是 systemd 更稳。4. 老派 rc.local 实现方式能用但 Ubuntu 默认不给开很多老教程会告诉你“开机执行脚本就把命令写进 /etc/rc.local 里”。这话在 CentOS 6、Ubuntu 14.04 时代是没问题的但放在现在的 Ubuntu 上默认情况下的 /etc/rc.local 根本不会被调用。4.1 为什么 Ubuntu 默认不跑 rc.local原因在于 systemd 接管了 init 流程后rc.local 变成了一个可选兼容层。系统里虽然还留着rc-local.service这个单元但它默认是disabled的。你直接改 /etc/rc.local 文件、加了脚本、设了执行权限重启后它就是不动除非你手动把 rc-local.service 启用起来。启用方法sudo systemctl enable rc-local.service sudo systemctl start rc-local.service同时确保/etc/rc.local文件存在、有执行权限、并且内容有正确的 shebang 和exit 0结尾#!/bin/bash /home/ubuntu/scripts/run1.sh exit 0chmod x /etc/rc.local也别忘了。4.2 rc.local 的运行时机和局限rc.local 的执行时机在 systemd 里是multi-user.target阶段比基础开机服务晚但比用户登录早。它最大的问题在于没有服务监控脚本启动后如果中途退出了系统不会告诉你不会自动重启也没有状态码。日志靠手动重定向rc.local 里执行命令的标准输出默认不接 journal出了问题只能靠自己在命令里 /path/to/log留痕。排错链路弱journalctl 里你能看到的只有rc-local.service这个单元是否执行成功看不到每个命令的细节。说句公道话rc.local 做个兜底入口还行但让我选我基本不会把它当主方案。现在 Ubuntu 18.04 之后 systemd 体系已经非常成熟服务单元的写法也不复杂花十分钟学会 systemd后面省心一百倍。5. 带图形桌面的开机启动desktop 文件自启如果你的 run1.sh 不是要跑在系统服务层面而是希望用户登录进 Ubuntu 桌面后自动执行那最正统的姿势是用.desktop文件放进自启动目录。5.1 autostart 目录的配置方法Ubuntu 桌面环境下自启动目录是~/.config/autostart/里面放的是标准的 desktop 入口文件。创建一个run1.desktopmkdir -p ~/.config/autostart nano ~/.config/autostart/run1.desktop内容[Desktop Entry] TypeApplication Namerun1 Exec/home/ubuntu/scripts/run1.sh X-GNOME-Autostart-enabledtrue CommentRun run1 script after login Terminalfalse保存后下次用户登录桌面时就会自动执行。这套机制跟 Windows 里的“启动”文件夹如出一辙对桌面用户来说非常直观。几个细节Exec后面必须写绝对路径desktop 文件不会帮你解析相对命令。Terminalfalse表示后台静默执行不会弹出终端窗口。如果脚本执行时间长或者你想看输出调成true也行但丑。如果脚本需要依赖桌面环境变量比如全局代理设置、IM 输入法状态这种方案天然兼容因为它是登录会话的一部分环境比 systemd/cron 完整得多。5.2 图形桌面自启与系统级自启的真正边界我用一个例子说明边界在哪。之前有个项目需要在一台 Ubuntu 工作站上跑一个内网穿透脚本脚本里面要读取用户家目录下~/.config/frp/frpc.ini并且要把日志写到用户目录下。有人图省事把它放在 systemd 服务里结果每次开机后写入的日志文件 owner 都变成 root用户自己用起来权限乱糟糟还得 sudo 才能删。这种场景就应该用桌面自启动或者至少是 user 级 systemd service~/.config/systemd/user/。搜热搜词里提到的entrypoint [sh, startup.sh -m standalone]那又是容器场景的 CMD 写法跟主机自启完全是两码事别混着看。所以定义边界很简单脚本需要 root、需要在用户登录前跑、需要被系统统一管理 → systemd 系统服务。脚本依赖当前登录用户的配置和会话环境 → desktop 自启动或 user 级 systemd 服务。脚本只是开机后执行一次的临时操作 → cron reboot。6. 重启后脚本没跑完整排查链路分享不管用了哪种方案总会有“配置完了以为没问题一重启发现屁都没跑”的时刻。别慌按下面的链路一步步查百分之九十九的问题都能定位。6.1 先从服务状态和日志下手如果你用的是 systemd第一件事就是查状态systemctl status run1.service journalctl -u run1.service -b-b表示只看本次开机后的日志非常有用于确认是不是这次启动才出的问题。常见的输出类型日志特征大概率原因处理方式Permission denied脚本无 x 权限chmod xNo such file or directory脚本路径写错或中间引用文件路径不对核对路径Exec format errorshebang 不对或脚本第一行写错检查#!/bin/bash什么输出都没有服务没有真正被触发检查 enable 是否成功Failed to start且 codeexited脚本内部命令异常检查脚本逻辑systemd 日志里还能看到退出码比如status1/FAILURE就说明脚本内部最后执行的命令返回了非零值。这种时候 systemd 本身没毛病是脚本业务逻辑挂了老老实实手动跑脚本去排查。6.2 手动执行脚本验证黄金路径无论配置文件写得多么天花乱坠脚本本身能跑通是前提。在终端里先手动执行一遍bash /home/ubuntu/scripts/run1.sh注意我用bash而不是直接./run1.sh这样即使脚本没有 x 权限也能验证内容逻辑。跑通之后再 chmod x再用./方式测一次。如果脚本里包含交互比如有read等待输入systemd 和 cron 下都会卡死。运行此类脚本前先确认脚本是否能非交互执行不能的话要么改脚本、要么改用 expect 之类的外壳包装。6.3 排查服务文件软链接是否建立enable成功后systemd 会在/etc/systemd/system/multi-user.target.wants/目录下创建一个指向服务文件的软链接ls -l /etc/systemd/system/multi-user.target.wants/run1.service如果没有这个链接说明 enable 失败了最常见的原因是服务文件里 [Install] 部分写错WantedBy拼错或者整段缺失。重新补上后记得再daemon-reload。这个操作对 cron、desktop 用户同样适用——凡是“配置文件写了但没生效”的先确认系统有没有把你写的配置加载进去。cron 就查crontab -ldesktop 就查ls ~/.config/autostart/。7. run1.sh 脚本本身的编写规范决定自启成败方案选得再好脚本写得稀烂照样翻车。我见过太多“开机脚本一直失败”的案例根因不在自启配置而在脚本自身。这里集中说几个最常见的坑提前帮你排掉。7.1 shebang、编码、换行符第一行必须写#!/bin/bash如果你把 Windows 上编辑好的 .sh 文件直接传到 Ubuntu 上大概率会遇到\r换行符问题脚本执行时报command not found。用sed -i s/\r$// run1.sh一键清理即可。7.2 日志与调试输出规范化自启环境没有交互终端脚本跑得怎么样全靠日志。不要在脚本里用裸echo输出至少重定向到固定文件exec /var/log/run1.log 21这行放在脚本靠前的位置后面所有 stdout、stderr 都会进 run1.log。排查时一刀切看一个文件比你在 systemd 和业务 log 之间来回翻舒服多了。7.3 幂等性重启后重复执行不会炸开机自启脚本一定要考虑“重复执行”场景。哪怕你的逻辑只是mkdir也要加判断或者加-p参数否则第二次执行时报错退出服务状态就是 failed。我自己的习惯是默认所有自启脚本都要设计成可重复执行的幂等脚本。加锁、判断目录是否存在、用pidof检查进程是否已在运行这些细节看着啰嗦但能在无数个清晨帮你省下大把debug时间。7.4 脚本结束状态不是最后一行的退出码systemd 判断 service 是否执行成功看的是 ExecStart 命令的退出码。如果你的脚本最后调用一个注定返回非零的命令比如 grep 找不到文件那么脚本整体退出码就是非零service 显示 failed哪怕你前面的逻辑全跑成功了。最好在脚本结尾加一行exit 0或者至少确保最后执行的命令是可控的。8. 重启验证与进一步优化让自启真正可靠配置好 systemd 服务后建议先做一次完整的重启验证。重启前先跑一遍journalctl --since 10 minutes ago之类确认当前没有报错重启后第一时间检查服务状态和脚本产生的结果文件。如果脚本执行得比预期晚——比如 reboot 之后过了几秒才看到日志——这很正常。systemd 服务启动有一个排序过程Afternetwork.target 并不是“开机完成”只是“网络准备阶段完成”。对绝大多数脚本来说这个时机的差异可以忽略。想做到更精细的启动控制有几个高级技巧用ExecStartPre在脚本执行前先验证前置条件比如网络连通性ExecStartPre/bin/bash -c until ping -c1 example.com /dev/null; do sleep 2; done用ExecStartPost在脚本完成后追加一些状态记录操作比如把服务执行成功的时间戳写进文件方便事后审计。一台机器上如果多个自启脚本之间有顺序依赖用After按序声明或者把它们合并在一个脚本里由内部逻辑控制顺序减少 systemd 单元数量。我个人在做多脚本自启时更倾向合并成一个总控入口比如一个startup.sh里面顺序调用 run1.sh、run2.sh…… 这样做的好处是排障链路短、服务单一时状态清晰。副作用是一个脚本挂了整串停摆所以内部最好逐个 catch 异常并记录日志再继续往下跑。如果你搜到的攻略让你在容器镜像里写ENTRYPOINT [sh, startup.sh]请注意那解决的是“容器启动时执行脚本”的问题跟主机开机自启是两回事。虽然思路相通都是启动阶段执行脚本但承载机制完全不同——容器靠 Dockerfile 的 ENTRYPOINT/CMD主机靠 systemd/cron/autostart。回到最初那个问题给 run1.sh 设置开机和重启自动执行在现在的 Ubuntu 上最值得养成的习惯就是——凡是系统级需求写 systemd 服务凡是登录会话级需求写 desktop autostart凡是一次性临时任务用 cron。搞清楚这三个的边界以后不管换到哪台机器、哪个发行版你都能在几分钟内搞定不用每次临时翻教程。
返回列表