
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/目錄下的一系列腳本這些腳本通常以SStart或KKill開頭后面跟著一個數字表示啟動或關閉的順序。/etc/rc.local在這個體系中是一個特殊的存在。它本身就是一個Shell腳本并且是在所有其他初始化腳本執行完畢之后、在系統準備接受用戶登錄之前執行的。因此它成為了系統管理員放置那些“不適合或來不及歸類到標準啟動腳本”的最后自定義操作的絕佳位置例如設置一個臨時的環境變量、啟動一個簡單的守護進程、或者執行一次性的初始化命令。因為它簡單直接——只需把命令寫進去賦予可執行權限它就會在開機時運行。2.2 systemd的登場與設計哲學systemd的出現是為了解決SysV init的一些固有缺陷啟動慢腳本順序執行、依賴關系管理復雜、并行化能力弱、對現代硬件如熱插拔、cgroups支持不足等。systemd引入了“單元文件”Unit File的概念將系統資源服務、掛載點、設備、套接字等統一抽象和管理。在systemd的架構中傳統的啟動流程被一系列目標target單元所替代。例如multi-user.target大致對應SysV init的運行級別3graphical.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 Documentationman:systemd-rc-local-generator(8) ConditionFileIsExecutable/etc/rc.local Afternetwork.target [Service] Typeforking ExecStart/etc/rc.local start TimeoutSec0 RemainAfterExityes GuessMainPIDno關鍵點解讀[Unit]部分:ConditionFileIsExecutable/etc/rc.local: 這是關鍵條件。該服務只有在/etc/rc.local文件存在且具有可執行權限時才會被激活和運行。Afternetwork.target: 指定本服務在network.target網絡服務就緒之后啟動這符合rc.local通常需要網絡功能的慣例。[Service]部分:Typeforking: 表明這是一個傳統的守護進程服務主進程會啟動子進程后退出。ExecStart/etc/rc.local start: 服務啟動時執行的命令就是運行我們熟悉的那個腳本。RemainAfterExityes: 即使主進程退出服務狀態仍標記為“active”這對于只運行一次就退出的腳本很重要。自動生成器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腳本首先我們需要創建這個“丟失”的腳本文件。創建文件使用你喜歡的文本編輯器如nano或vim以root權限創建文件。sudo nano /etc/rc.local編寫腳本內容將以下模板內容粘貼進去。務必保留開頭的shebang#!/bin/bash這是告訴系統用哪個解釋器來執行腳本。#!/bin/bash # # rc.local - 在系統啟動后、用戶登錄前執行的自定義腳本 # # 請在此處添加你需要開機自啟動的命令。 # 例如 # 啟動一個自定義服務 # /path/to/your/daemon --start # # 設置一個環境變量對所有用戶生效但僅限于此會話 # export MY_VARsome_value # # 掛載一個網絡驅動器 # mount -t cifs //server/share /mnt/share -o usernameuser,passwordpass # # 注意如果命令需要長時間運行守護進程請確保它們被正確地放到后臺 # 或者使用‘’符號否則會阻塞啟動過程。 # 更推薦的做法是為長時間運行的服務創建獨立的systemd服務單元。 # 示例在啟動時向系統日志寫入一條消息 logger -t rc.local “系統啟動完成正在執行rc.local腳本” # 你的自定義命令寫在這里 # echo “Hello from rc.local” /tmp/rc.local.test exit 0重要提示腳本最后一行必須是exit 0。在Shell腳本中exit 0表示腳本成功退出。systemd會檢查這個返回值如果返回非零值可能會認為服務啟動失敗。賦予可執行權限這是激活rc-local.service條件的關鍵一步。sudo chmod x /etc/rc.local執行完這一步理論上systemd-rc-local-generator就已經能檢測到并啟用該服務了。3.2 驗證服務狀態與測試創建文件并授權后我們不應該直接重啟而是先驗證服務狀態。檢查服務單元狀態sudo systemctl status rc-local如果服務是active (exited)恭喜服務已經成功運行過一次可能是在之前的啟動中。這通常是正常狀態因為rc.local腳本執行完就退出了。如果服務是inactive (dead)這也很正常表示服務尚未被觸發運行。我們可以手動啟動它來測試。如果服務顯示為failed或其他錯誤需要查看日志排查。手動測試腳本最安全的測試方法是直接以root身份運行腳本看是否有語法錯誤或命令錯誤。sudo /etc/rc.local觀察輸出確保命令都按預期執行。如果腳本中有像mount這樣的命令可能需要先確保依賴條件如網絡已滿足。啟用服務并重啟驗證雖然理論上不需要手動啟用但為了確保萬無一失可以執行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生態中為每個獨立的服務創建獨立的單元文件是更推薦的做法。腳本中的命令如果命令需要網絡確保Afternetwork.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的服務。創建服務單元文件單元文件通常放在/etc/systemd/system/目錄下以.service結尾。sudo nano /etc/systemd/system/myapp.service編寫服務單元內容這是一個功能相對完整的示例。[Unit] DescriptionMy Custom Python Application Documentationhttps://example.com/myapp/docs Afternetwork-online.target mysql.service # 在網絡就緒、MySQL啟動后啟動 Wantsnetwork-online.target # 希望網絡在線但不強依賴 Requiresmysql.service # 強依賴MySQLMySQL啟動失敗則本服務不啟動 [Service] Typesimple Usermyappuser # 指定運行用戶增強安全性需先創建此用戶 Groupmyappuser WorkingDirectory/opt/myapp ExecStart/usr/bin/python3 /opt/myapp/app.py # 環境變量文件可以存放數據庫密碼等敏感信息 EnvironmentFile/etc/default/myapp # 重啟策略總是重啟除非被手動停止 Restartalways RestartSec10 # 重啟前等待10秒 # 資源限制 LimitNOFILE65536 LimitNPROC4096 # 標準輸出和錯誤輸出重定向到系統日志journal StandardOutputjournal StandardErrorjournal # 或者重定向到文件 # StandardOutputfile:/var/log/myapp.log # StandardErrorfile:/var/log/myapp.err.log [Install] WantedBymulti-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是最常用的。創建運行用戶和環境文件可選但推薦sudo adduser --system --no-create-home --group myappuser sudo nano /etc/default/myapp在/etc/default/myapp中添加DB_PASSWORDsecret123設置權限和重載配置sudo chown root:root /etc/systemd/system/myapp.service sudo chmod 644 /etc/systemd/system/myapp.service sudo systemctl daemon-reload # 必須讓systemd識別新服務4.3 管理、測試與調試自定義服務啟動、停止、重啟服務sudo systemctl start myapp sudo systemctl stop myapp sudo systemctl restart myapp啟用/禁用開機自啟sudo systemctl enable myapp # 啟用自啟 sudo systemctl disable myapp # 禁用自啟查看服務狀態和日志sudo systemctl status myapp這個命令會顯示服務是否活躍、主進程PID、以及最近的幾條日志。sudo journalctl -u myapp -f # 實時跟蹤該服務的日志 sudo journalctl -u myapp --since today # 查看今天的日志 sudo journalctl -u myapp -p err # 只看錯誤級別以上的日志journalctl是systemd強大的日志工具是排查服務問題的利器。驗證依賴關系使用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但它有幾個缺點時機不確定cron的reboot在系統啟動過程的哪個階段運行沒有嚴格定義可能早于網絡就緒。環境變量有限cron任務的環境變量非常精簡可能缺少PATH或其他關鍵變量。缺乏依賴管理無法指定必須在某個服務如網絡之后運行。Systemd Timer則能很好地解決這些問題。5.2 創建reboot定時器實例假設我們有一個腳本/usr/local/bin/cleanup-tmp.sh需要在每次啟動后清理/tmp目錄下超過7天的文件。創建服務單元文件首先為要執行的任務創建一個.service文件。注意Type設為oneshot表示一次性任務。sudo nano /etc/systemd/system/cleanup-tmp.service[Unit] DescriptionCleanup old files in /tmp Afternetwork-online.target # 確保有網絡如果需要 Requiresnetwork-online.target [Service] Typeoneshot ExecStart/usr/local/bin/cleanup-tmp.sh Usernobody # 使用最小權限用戶 Groupnogroup [Install] WantedBymulti-user.target創建定時器單元文件然后創建一個同名的.timer文件來定義觸發時機。sudo nano /etc/systemd/system/cleanup-tmp.timer[Unit] DescriptionRun cleanup-tmp at boot Requirescleanup-tmp.service [Timer] OnBootSec5min # 系統啟動后5分鐘執行 # OnBootSec0 # 如果需要在啟動后立即執行可以設為0 Unitcleanup-tmp.service [Install] WantedBytimers.target關鍵參數OnBootSec定義了啟動后多久觸發。這里設為5分鐘是給系統一個完全穩定下來的時間。編寫執行腳本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啟用定時器而非服務sudo systemctl daemon-reload sudo systemctl enable cleanup-tmp.timer # 啟用定時器 sudo systemctl start cleanup-tmp.timer # 立即激活定時器檢查定時器狀態systemctl list-timers --all你會看到cleanup-tmp.timer在列表中并顯示下次觸發時間例如“5min left”。5.3 此方法的適用場景與優勢適用場景啟動后的一次性初始化任務、定期維護任務也可以結合OnCalendar做定期執行、與系統啟動強相關但非持續運行的任務。優勢精確的啟動時機控制可以通過After和OnBootSec精確控制任務在啟動流程中的執行點。完整的systemd特性享受服務單元的所有好處如依賴管理、資源控制、集中日志。更清晰的管理.timer和.service分離邏輯清晰。可以用systemctl list-timers統一管理所有定時任務。6. 常見問題排查與深度優化技巧在實際操作中你可能會遇到各種問題。這里匯總了一些常見坑點及其解決方法。6.1 服務啟動失敗排查流程當你的rc.local或自定義服務沒有按預期運行時請按以下順序排查檢查服務狀態sudo systemctl status service-name。這是第一步通常會給出錯誤線索如“failed to start”、“codeexited, status203/EXEC”等。查看詳細日志sudo journalctl -u service-name -xe。-xe參數會顯示更詳細的上下文和錯誤信息是定位問題的關鍵。手動執行腳本以服務指定的User身份如果沒有指定就是root手動運行ExecStart中的命令看是否有權限、路徑或語法錯誤。sudo -u myappuser /usr/bin/python3 /opt/myapp/app.py檢查依賴服務如果服務定義了After或Requires確保這些依賴服務本身狀態正常。檢查單元文件語法使用systemd-analyze verify /path/to/service.service可以檢查單元文件的基本語法錯誤。檢查SELinux/AppArmor在某些嚴格的安全策略下你的腳本或服務可能被阻止。查看/var/log/audit/audit.logSELinux或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.service只Afternetwork.target而network.target只表示網絡服務啟動不一定代表網絡連接就緒。對于必須聯網的命令可能需要修改服務單元使用network-online.target。這是一個高頻坑點6.3 修改rc-local.service的依賴使用Drop-in文件如果需要讓rc.local在網絡真正在線后執行不要直接修改/lib/systemd/system/rc-local.service。正確的方法是創建“drop-in”覆蓋文件。創建覆蓋目錄和文件sudo mkdir -p /etc/systemd/system/rc-local.service.d sudo nano /etc/systemd/system/rc-local.service.d/override.conf寫入覆蓋配置[Unit] # 增加對network-online.target的依賴并等待它完成 Afternetwork-online.target Wantsnetwork-online.target這個配置會與原始單元文件合并增加新的依賴關系。重新加載并重啟服務sudo systemctl daemon-reload sudo systemctl restart rc-local # 如果服務正在運行6.4 自定義服務單元的最佳實踐與高級技巧使用Typenotify如果你的應用程序支持systemd通知協議如使用sd_notify可以將Type設為notify。這樣應用程序啟動完成后會主動通知systemdsystemd能更準確地判斷服務狀態。設置資源限制在[Service]部分使用LimitCPU,LimitFSIZE,LimitDATA,LimitSTACK等指令防止服務耗盡系統資源。設置工作目錄和umaskWorkingDirectory確保程序在正確的路徑下運行。UMask可以控制創建文件的默認權限。使用PrivateTmp,ProtectSystem等加強安全這些選項可以為服務創建一個私有的/tmp目錄或限制其對系統文件的寫權限極大地提升安全性。環境變量管理對于復雜的應用使用Environment或EnvironmentFile來管理環境變量比在腳本里寫死要清晰和安全得多。處理長時間啟動的服務如果服務啟動很慢可能需要增加TimeoutStartSec的值避免systemd誤認為啟動超時而殺死進程。6.5 調試與日志分析實戰假設你的自定義服務myapp啟動失敗狀態顯示codeexited, status127。查看日志sudo journalctl -u myapp -xe。你可能會看到類似“/usr/bin/python3: No such file or directory”的錯誤。這說明ExecStart中的命令路徑不對。檢查路徑使用which python3確認解釋器的準確路徑。檢查腳本權限確保app.py有可執行權限或者ExecStart中明確使用了python3解釋器。檢查用戶權限如果服務以myappuser運行確保該用戶對/opt/myapp目錄和其中的文件有讀取和執行權限。分步調試在單元文件的ExecStart前加上/bin/bash -x來調試腳本但更推薦的做法是將調試命令寫入腳本并重定向輸出到文件例如在腳本開頭添加exec /tmp/debug.log 21。從“找不到rc.local”這個具體問題出發我們實際上完成了一次從傳統Linux服務管理到現代systemd體系的深度探索。在Ubuntu 20.04及以后的版本中rc.local更像是一個為了兼容性而保留的“快捷方式”其背后是一套更強大、更復雜的systemd服務管理機制。對于簡單的啟動任務恢復rc.local并了解其原理是快速解決問題的好辦法。但對于任何嚴肅的、需要長期運行或具備一定復雜度的任務投入時間學習并創建自定義的systemd服務單元絕對是值得的。它不僅能讓你的服務更穩定、更易管理也是深入理解現代Linux系統運維的必經之路。下次再遇到服務啟動的問題不妨先打開journalctl看看日志你會發現解決問題的思路清晰了很多。