Ubuntu 20.04开机自启动:从rc.local到systemd服务管理的完整指南
2026/8/11 5:18:47 网站建设 项目流程

1. 项目概述:当Ubuntu 20.04的rc.local“消失”时

如果你和我一样,从更早的Ubuntu版本或者像CentOS这样的发行版迁移过来,第一次在Ubuntu 20.04上想编辑/etc/rc.local文件来添加开机自启动脚本时,大概率会愣住——这个文件根本不存在。这可不是你的系统安装出了问题,而是Ubuntu从某个版本开始,对系统启动和服务管理方式的一次重大转向。rc.local这个曾经在几乎所有Linux发行版中都扮演着“开机最后一道工序”角色的老朋友,在拥抱了systemd的现代Ubuntu世界里,其地位和实现方式已经发生了根本性的变化。

简单来说,Ubuntu 20.04默认没有rc.local文件,是因为它默认使用systemd作为初始化系统(init system)。在systemd的体系里,传统的/etc/rc.local脚本不再是启动流程的固有部分。但这绝不意味着我们失去了在系统启动后、用户登录前自动执行自定义脚本的能力。恰恰相反,systemd提供了一套更强大、更规范、也更可靠的机制来实现这个需求。对于系统管理员、开发者,甚至是需要在服务器或开发机上部署后台服务的普通用户,理解并掌握这套新机制,是绕过这个“坑”并走向更佳实践的关键。本文将带你从零开始,不仅找回“丢失”的rc.local功能,更深入理解其背后的原理,并掌握在systemd时代正确、优雅地管理自启动服务的方法。

2. 核心原理:从SysV init到systemd的演进

要彻底解决rc.local缺失的问题,我们不能停留在“创建一个文件”的表面操作上,必须理解其背后的技术变迁。这有助于我们在未来面对其他类似的服务管理问题时,能够举一反三。

2.1 传统的SysV init与rc.local

systemd成为主流之前,大多数Linux发行版使用基于SysV init的启动系统。它的核心思想是“运行级别”(Runlevel),比如运行级别3代表多用户文本模式,运行级别5代表图形界面。系统启动时,init进程会按照特定的顺序执行/etc/rc.d//etc/init.d/目录下的一系列脚本,这些脚本通常以S(Start)或K(Kill)开头,后面跟着一个数字表示启动或关闭的顺序。

/etc/rc.local在这个体系中是一个特殊的存在。它本身就是一个Shell脚本,并且是在所有其他初始化脚本执行完毕之后、在系统准备接受用户登录之前执行的。因此,它成为了系统管理员放置那些“不适合或来不及归类到标准启动脚本”的最后自定义操作的绝佳位置,例如设置一个临时的环境变量、启动一个简单的守护进程、或者执行一次性的初始化命令。因为它简单直接——只需把命令写进去,赋予可执行权限,它就会在开机时运行。

2.2 systemd的登场与设计哲学

systemd的出现是为了解决SysV init的一些固有缺陷:启动慢(脚本顺序执行)、依赖关系管理复杂、并行化能力弱、对现代硬件(如热插拔、cgroups)支持不足等。systemd引入了“单元文件”(Unit File)的概念,将系统资源(服务、挂载点、设备、套接字等)统一抽象和管理。

systemd的架构中,传统的启动流程被一系列目标(target)单元所替代。例如,multi-user.target大致对应SysV init的运行级别3,graphical.target对应运行级别5。服务的启动不再是简单的脚本顺序执行,而是由systemd根据单元文件中定义的依赖关系,智能地、尽可能并行地启动。

那么,rc.local去哪了?在完全拥抱systemd的发行版(如Ubuntu 16.04及以后、RHEL/CentOS 7及以后)中,rc.local服务本身就是一个可选的systemd服务单元。也就是说,rc.local的功能被“服务化”了。默认情况下,这个服务单元文件是存在的(/lib/systemd/system/rc-local.service),但它可能没有被启用,而且其指向的脚本文件(/etc/rc.local)也不存在,这就导致了我们开头遇到的问题。

2.3 rc-local.service服务单元解析

让我们来看一下这个服务单元的核心内容(你可以通过cat /lib/systemd/system/rc-local.service查看):

# SPDX-License-Identifier: LGPL-2.1-or-later # # This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. # This unit gets pulled automatically into multi-user.target by # systemd-rc-local-generator if /etc/rc.local is executable. [Unit] Description=/etc/rc.local Compatibility Documentation=man:systemd-rc-local-generator(8) ConditionFileIsExecutable=/etc/rc.local After=network.target [Service] Type=forking ExecStart=/etc/rc.local start TimeoutSec=0 RemainAfterExit=yes GuessMainPID=no

关键点解读:

  1. [Unit]部分:
    • ConditionFileIsExecutable=/etc/rc.local: 这是关键条件。该服务只有在/etc/rc.local文件存在且具有可执行权限时,才会被激活和运行。
    • After=network.target: 指定本服务在network.target(网络服务就绪)之后启动,这符合rc.local通常需要网络功能的惯例。
  2. [Service]部分:
    • Type=forking: 表明这是一个传统的守护进程服务,主进程会启动子进程后退出。
    • ExecStart=/etc/rc.local start: 服务启动时执行的命令,就是运行我们熟悉的那个脚本。
    • RemainAfterExit=yes: 即使主进程退出,服务状态仍标记为“active”,这对于只运行一次就退出的脚本很重要。
  3. 自动生成器(Generator): 注释中提到,这个单元是由systemd-rc-local-generator在满足条件时自动拉取到multi-user.target的。这意味着你不需要手动systemctl enable rc-local,只要脚本可执行,它就会在开机时被调用。

所以,问题的根源很清晰:Ubuntu 20.04提供了rc-local.service这个兼容性服务,但默认没有提供可执行的/etc/rc.local脚本文件。我们的任务就是补全这个链条。

注意:直接修改/lib/systemd/system/下的单元文件不是好习惯,因为系统更新可能会覆盖你的修改。最佳实践是使用systemctl edit命令或创建/etc/systemd/system/下的覆盖文件(drop-in file)。但对于恢复rc.local,我们只需要创建脚本文件即可。

3. 解决方案一:恢复传统的rc.local方式

这是最直接、最符合旧有习惯的方法,适合那些希望快速将原有rc.local脚本迁移到Ubuntu 20.04的用户。

3.1 创建并配置/etc/rc.local脚本

首先,我们需要创建这个“丢失”的脚本文件。

  1. 创建文件:使用你喜欢的文本编辑器(如nanovim)以root权限创建文件。

    sudo nano /etc/rc.local
  2. 编写脚本内容:将以下模板内容粘贴进去。务必保留开头的shebang(#!/bin/bash,这是告诉系统用哪个解释器来执行脚本。

    #!/bin/bash # # rc.local - 在系统启动后、用户登录前执行的自定义脚本 # # 请在此处添加你需要开机自启动的命令。 # 例如: # 启动一个自定义服务 # /path/to/your/daemon --start # # 设置一个环境变量(对所有用户生效,但仅限于此会话) # export MY_VAR="some_value" # # 挂载一个网络驱动器 # mount -t cifs //server/share /mnt/share -o username=user,password=pass # # 注意:如果命令需要长时间运行(守护进程),请确保它们被正确地放到后台, # 或者使用‘&’符号,否则会阻塞启动过程。 # 更推荐的做法是为长时间运行的服务创建独立的systemd服务单元。 # 示例:在启动时向系统日志写入一条消息 logger -t rc.local “系统启动完成,正在执行rc.local脚本” # 你的自定义命令写在这里 # echo “Hello from rc.local” > /tmp/rc.local.test exit 0

    重要提示:脚本最后一行必须是exit 0。在Shell脚本中,exit 0表示脚本成功退出。systemd会检查这个返回值,如果返回非零值,可能会认为服务启动失败。

  3. 赋予可执行权限:这是激活rc-local.service条件的关键一步。

    sudo chmod +x /etc/rc.local

    执行完这一步,理论上systemd-rc-local-generator就已经能检测到并启用该服务了。

3.2 验证服务状态与测试

创建文件并授权后,我们不应该直接重启,而是先验证服务状态。

  1. 检查服务单元状态

    sudo systemctl status rc-local
    • 如果服务是active (exited):恭喜,服务已经成功运行过一次(可能是在之前的启动中)。这通常是正常状态,因为rc.local脚本执行完就退出了。
    • 如果服务是inactive (dead):这也很正常,表示服务尚未被触发运行。我们可以手动启动它来测试。
    • 如果服务显示为failed或其他错误:需要查看日志排查。
  2. 手动测试脚本:最安全的测试方法是直接以root身份运行脚本,看是否有语法错误或命令错误。

    sudo /etc/rc.local

    观察输出,确保命令都按预期执行。如果脚本中有像mount这样的命令,可能需要先确保依赖条件(如网络)已满足。

  3. 启用服务并重启验证

    • 虽然理论上不需要手动启用,但为了确保万无一失,可以执行:
      sudo systemctl enable rc-local
      这会创建必要的符号链接,确保服务在启动时被调用。
    • 重启系统或重新加载systemd配置后验证:
      sudo systemctl daemon-reload # 重新加载systemd配置(在修改单元文件后才需要,我们只改了脚本,通常不需要) sudo systemctl start rc-local # 手动启动一次 sudo systemctl status rc-local # 再次检查状态
    • 查看脚本执行日志:
      sudo journalctl -u rc-local -e
      使用-e参数跳转到日志末尾,查看最近的记录。你应该能看到脚本中logger命令输出的信息,以及其他命令的执行结果或错误。

3.3 此方法的优缺点与注意事项

优点

  • 简单直观:对于从旧系统迁移过来的脚本,几乎可以无缝迁移。
  • 集中管理:所有开机自启动命令放在一个文件里,易于查看。

缺点与注意事项

  • 缺乏精细控制:无法方便地设置依赖关系(如“必须在MySQL启动后运行”)、重启策略、资源限制等。
  • 错误处理弱:如果脚本中某条命令失败,整个脚本可能中断,且错误信息可能不够清晰。
  • 不符合现代服务管理规范:在systemd生态中,为每个独立的服务创建独立的单元文件是更推荐的做法。
  • 脚本中的命令:如果命令需要网络,确保After=network.target足够(有时可能需要network-online.target)。对于要放到后台的守护进程,务必处理好,避免阻塞。

实操心得:我曾在rc.local里启动一个Java应用,但没有正确使用nohup&,导致启动卡住,系统无法进入登录界面。排查了很久才发现是rc.local脚本没有退出。因此,对于任何可能长时间运行或交互式的命令,一定要确保它们不会阻塞脚本进程。

4. 解决方案二:拥抱systemd,创建自定义服务单元

对于更复杂、更需要可靠管理的自启动任务(例如运行一个Web服务器、一个数据库、一个自定义的监控代理),强烈推荐直接使用systemd服务单元。这是更专业、更强大的方法。

4.1 为何要创建自定义服务单元?

假设你有一个Python脚本/opt/myapp/app.py,需要它在开机时启动并在后台一直运行。用rc.local你可能会写:

python3 /opt/myapp/app.py &

但这有几个问题:进程崩溃了不会自动重启;日志输出混在系统日志里难以查看;无法方便地设置内存或CPU限制;无法定义它和别的服务的启动顺序。

而创建一个systemd服务单元可以完美解决所有这些问题。

4.2 创建自定义服务单元步骤详解

我们将为/opt/myapp/app.py创建一个名为myapp的服务。

  1. 创建服务单元文件:单元文件通常放在/etc/systemd/system/目录下,以.service结尾。

    sudo nano /etc/systemd/system/myapp.service
  2. 编写服务单元内容:这是一个功能相对完整的示例。

    [Unit] Description=My Custom Python Application Documentation=https://example.com/myapp/docs After=network-online.target mysql.service # 在网络就绪、MySQL启动后启动 Wants=network-online.target # 希望网络在线,但不强依赖 Requires=mysql.service # 强依赖MySQL,MySQL启动失败则本服务不启动 [Service] Type=simple User=myappuser # 指定运行用户,增强安全性(需先创建此用户) Group=myappuser WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 /opt/myapp/app.py # 环境变量文件,可以存放数据库密码等敏感信息 EnvironmentFile=/etc/default/myapp # 重启策略:总是重启,除非被手动停止 Restart=always RestartSec=10 # 重启前等待10秒 # 资源限制 LimitNOFILE=65536 LimitNPROC=4096 # 标准输出和错误输出重定向到系统日志(journal) StandardOutput=journal StandardError=journal # 或者重定向到文件 # StandardOutput=file:/var/log/myapp.log # StandardError=file:/var/log/myapp.err.log [Install] WantedBy=multi-user.target

    关键参数解析

    • [Unit]:
      • After: 定义启动顺序。
      • Wants: 弱依赖。网络不在线,服务也会启动,但可能功能异常。
      • Requires: 强依赖。MySQL启动失败,本服务将不会启动。
    • [Service]:
      • Type:simple(默认)表示ExecStart的进程是主服务进程。还有forking,oneshot,notify等类型。
      • User/Group:极其重要的安全实践。不要用root运行你的应用。
      • Restart: 控制服务失败后的重启行为。always是最常用的。
      • EnvironmentFile: 将敏感配置与单元文件分离的好方法。
    • [Install]:
      • WantedBy: 定义当启用(systemctl enable)时,该服务链接到哪个目标(target)。multi-user.target是最常用的。
  3. 创建运行用户和环境文件(可选但推荐)

    sudo adduser --system --no-create-home --group myappuser sudo nano /etc/default/myapp

    /etc/default/myapp中添加:DB_PASSWORD=secret123

  4. 设置权限和重载配置

    sudo chown root:root /etc/systemd/system/myapp.service sudo chmod 644 /etc/systemd/system/myapp.service sudo systemctl daemon-reload # 必须!让systemd识别新服务

4.3 管理、测试与调试自定义服务

  1. 启动、停止、重启服务

    sudo systemctl start myapp sudo systemctl stop myapp sudo systemctl restart myapp
  2. 启用/禁用开机自启

    sudo systemctl enable myapp # 启用自启 sudo systemctl disable myapp # 禁用自启
  3. 查看服务状态和日志

    sudo systemctl status myapp

    这个命令会显示服务是否活跃、主进程PID、以及最近的几条日志。

    sudo journalctl -u myapp -f # 实时跟踪该服务的日志 sudo journalctl -u myapp --since today # 查看今天的日志 sudo journalctl -u myapp -p err # 只看错误级别以上的日志

    journalctlsystemd强大的日志工具,是排查服务问题的利器。

  4. 验证依赖关系:使用systemctl list-dependencies可以查看服务的依赖树。

4.4 此方法的优势

  • 强大的生命周期管理:自动重启、资源限制、安全上下文(User/Group)。
  • 清晰的依赖关系:确保服务按正确顺序启动。
  • 集中化的日志:通过journalctl统一查看和管理日志。
  • 标准化操作start,stop,restart,enable,disable,操作统一。
  • 更高的可靠性:是生产环境部署服务的标准方式。

5. 解决方案三:使用systemd定时器(@reboot)替代

有些任务并不需要作为一个常驻服务(daemon)运行,它们只需要在每次系统启动时执行一次,执行完就结束。例如:清理临时目录、发送启动通知、初始化一些设备状态。对于这种需求,除了rc.local,还有一个非常systemd风格的选择:Systemd Timer,特别是结合@reboot指令的定时器。

5.1 Cron的@reboot与Systemd Timer对比

传统的cron也支持@reboot,但它有几个缺点:

  1. 时机不确定:cron的@reboot在系统启动过程的哪个阶段运行,没有严格定义,可能早于网络就绪。
  2. 环境变量有限:cron任务的环境变量非常精简,可能缺少PATH或其他关键变量。
  3. 缺乏依赖管理:无法指定必须在某个服务(如网络)之后运行。

Systemd Timer则能很好地解决这些问题。

5.2 创建@reboot定时器实例

假设我们有一个脚本/usr/local/bin/cleanup-tmp.sh,需要在每次启动后清理/tmp目录下超过7天的文件。

  1. 创建服务单元文件:首先,为要执行的任务创建一个.service文件。注意Type设为oneshot,表示一次性任务。

    sudo nano /etc/systemd/system/cleanup-tmp.service
    [Unit] Description=Cleanup old files in /tmp After=network-online.target # 确保有网络(如果需要) Requires=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/cleanup-tmp.sh User=nobody # 使用最小权限用户 Group=nogroup [Install] WantedBy=multi-user.target
  2. 创建定时器单元文件:然后,创建一个同名的.timer文件来定义触发时机。

    sudo nano /etc/systemd/system/cleanup-tmp.timer
    [Unit] Description=Run cleanup-tmp at boot Requires=cleanup-tmp.service [Timer] OnBootSec=5min # 系统启动后5分钟执行 # OnBootSec=0 # 如果需要在启动后立即执行,可以设为0 Unit=cleanup-tmp.service [Install] WantedBy=timers.target

    关键参数OnBootSec定义了启动后多久触发。这里设为5分钟,是给系统一个完全稳定下来的时间。

  3. 编写执行脚本

    sudo nano /usr/local/bin/cleanup-tmp.sh
    #!/bin/bash # 清理/tmp下超过7天的文件 find /tmp -type f -mtime +7 -delete 2>/dev/null || true find /tmp -type d -empty -mtime +7 -delete 2>/dev/null || true logger -t cleanup-tmp “Cleaned up old files in /tmp”
    sudo chmod +x /usr/local/bin/cleanup-tmp.sh
  4. 启用定时器(而非服务)

    sudo systemctl daemon-reload sudo systemctl enable cleanup-tmp.timer # 启用定时器 sudo systemctl start cleanup-tmp.timer # 立即激活定时器
  5. 检查定时器状态

    systemctl list-timers --all

    你会看到cleanup-tmp.timer在列表中,并显示下次触发时间(例如“5min left”)。

5.3 此方法的适用场景与优势

适用场景:启动后的一次性初始化任务、定期维护任务(也可以结合OnCalendar做定期执行)、与系统启动强相关但非持续运行的任务。

优势

  • 精确的启动时机控制:可以通过AfterOnBootSec精确控制任务在启动流程中的执行点。
  • 完整的systemd特性:享受服务单元的所有好处,如依赖管理、资源控制、集中日志。
  • 更清晰的管理.timer.service分离,逻辑清晰。可以用systemctl list-timers统一管理所有定时任务。

6. 常见问题排查与深度优化技巧

在实际操作中,你可能会遇到各种问题。这里汇总了一些常见坑点及其解决方法。

6.1 服务启动失败排查流程

当你的rc.local或自定义服务没有按预期运行时,请按以下顺序排查:

  1. 检查服务状态sudo systemctl status service-name。这是第一步,通常会给出错误线索,如“failed to start”、“code=exited, status=203/EXEC”等。
  2. 查看详细日志sudo journalctl -u service-name -xe-xe参数会显示更详细的上下文和错误信息,是定位问题的关键。
  3. 手动执行脚本:以服务指定的User身份(如果没有指定,就是root)手动运行ExecStart中的命令,看是否有权限、路径或语法错误。
    sudo -u myappuser /usr/bin/python3 /opt/myapp/app.py
  4. 检查依赖服务:如果服务定义了AfterRequires,确保这些依赖服务本身状态正常。
  5. 检查单元文件语法:使用systemd-analyze verify /path/to/service.service可以检查单元文件的基本语法错误。
  6. 检查SELinux/AppArmor:在某些严格的安全策略下,你的脚本或服务可能被阻止。查看/var/log/audit/audit.log(SELinux)或journalctl中AppArmor的DENIED信息。可以尝试暂时将安全策略设置为宽容模式测试。

6.2 rc.local脚本不执行的典型原因

  • 文件权限问题/etc/rc.local没有可执行权限(chmod +x)。
  • 脚本语法错误:特别是shebang#!/bin/bash写错或缺失,或者脚本最后没有exit 0
  • 命令路径问题:脚本中使用了相对路径或未在root用户的PATH环境变量中的命令。务必使用绝对路径
  • 服务未激活:虽然理论上可执行文件存在就会激活,但有时systemd-rc-local-generator可能没生效。可以尝试手动sudo systemctl enable rc-local
  • 执行时机过早:脚本中的命令需要网络,但rc-local.serviceAfter=network.target,而network.target只表示网络服务启动,不一定代表网络连接就绪。对于必须联网的命令,可能需要修改服务单元,使用network-online.target(这是一个高频坑点!)

6.3 修改rc-local.service的依赖(使用Drop-in文件)

如果需要让rc.local在网络真正在线后执行,不要直接修改/lib/systemd/system/rc-local.service。正确的方法是创建“drop-in”覆盖文件。

  1. 创建覆盖目录和文件

    sudo mkdir -p /etc/systemd/system/rc-local.service.d sudo nano /etc/systemd/system/rc-local.service.d/override.conf
  2. 写入覆盖配置

    [Unit] # 增加对network-online.target的依赖,并等待它完成 After=network-online.target Wants=network-online.target

    这个配置会与原始单元文件合并,增加新的依赖关系。

  3. 重新加载并重启服务

    sudo systemctl daemon-reload sudo systemctl restart rc-local # 如果服务正在运行

6.4 自定义服务单元的最佳实践与高级技巧

  1. 使用Type=notify:如果你的应用程序支持systemd通知协议(如使用sd_notify),可以将Type设为notify。这样应用程序启动完成后会主动通知systemdsystemd能更准确地判断服务状态。
  2. 设置资源限制:在[Service]部分使用LimitCPU,LimitFSIZE,LimitDATA,LimitSTACK等指令,防止服务耗尽系统资源。
  3. 设置工作目录和umaskWorkingDirectory确保程序在正确的路径下运行。UMask可以控制创建文件的默认权限。
  4. 使用PrivateTmp,ProtectSystem等加强安全:这些选项可以为服务创建一个私有的/tmp目录,或限制其对系统文件的写权限,极大地提升安全性。
  5. 环境变量管理:对于复杂的应用,使用Environment=EnvironmentFile=来管理环境变量,比在脚本里写死要清晰和安全得多。
  6. 处理长时间启动的服务:如果服务启动很慢,可能需要增加TimeoutStartSec的值,避免systemd误认为启动超时而杀死进程。

6.5 调试与日志分析实战

假设你的自定义服务myapp启动失败,状态显示code=exited, status=127

  1. 查看日志sudo journalctl -u myapp -xe。你可能会看到类似“/usr/bin/python3: No such file or directory”的错误。这说明ExecStart中的命令路径不对。
  2. 检查路径:使用which python3确认解释器的准确路径。
  3. 检查脚本权限:确保app.py有可执行权限,或者ExecStart中明确使用了python3解释器。
  4. 检查用户权限:如果服务以myappuser运行,确保该用户对/opt/myapp目录和其中的文件有读取和执行权限。
  5. 分步调试:在单元文件的ExecStart前加上/bin/bash -x来调试脚本,但更推荐的做法是将调试命令写入脚本,并重定向输出到文件,例如在脚本开头添加exec > /tmp/debug.log 2>&1

从“找不到rc.local”这个具体问题出发,我们实际上完成了一次从传统Linux服务管理到现代systemd体系的深度探索。在Ubuntu 20.04及以后的版本中,rc.local更像是一个为了兼容性而保留的“快捷方式”,其背后是一套更强大、更复杂的systemd服务管理机制。对于简单的启动任务,恢复rc.local并了解其原理是快速解决问题的好办法。但对于任何严肃的、需要长期运行或具备一定复杂度的任务,投入时间学习并创建自定义的systemd服务单元,绝对是值得的。它不仅能让你的服务更稳定、更易管理,也是深入理解现代Linux系统运维的必经之路。下次再遇到服务启动的问题,不妨先打开journalctl看看日志,你会发现解决问题的思路清晰了很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询