系統服務
書接上文: linux自啟任務詳解
演示系統:ubuntu 20.04
開發部署項目的時候常常有這樣的場景: 業務功能以后臺服務的形式提供,部署完成后可以隨著系統的重啟而自動啟動;服務異常掛掉后可以再次拉起
這個功能在ubuntu系統中通常由systemd提供
如果僅僅需要達成上述的場景功能,則systemd的自定義服務就可以滿足
什么是systemd
systemd:系統和服務管理器
- 功能:
systemd 是一個初始化系統(init system)和服務管理器,它負責在 Linux 系統啟動時啟動系統的核心服務和進程。它的任務是管理系統引導、服務管理、進程監控、資源管理等。
systemd 提供了服務啟動、停止、重啟、日志記錄等功能,并管理系統的運行狀態。 - 作用:
啟動和管理系統服務:systemd 會在系統啟動時根據配置文件(服務單元文件)啟動必要的系統服務(例如網絡、日志記錄、定時任務等)。
管理進程和依賴關系:systemd 確保服務按照正確的順序啟動,并且根據需要重啟或停止。
資源管理:通過 cgroups(控制組)和其他技術,systemd 能夠限制服務對 CPU、內存等資源的使用。 - 配置文件:
systemd 使用以 .service 結尾的單元文件(unit files)來定義服務。每個服務有一個單獨的配置文件,這些文件描述了服務如何啟動、停止、重啟等。
例如,/etc/systemd/system/ 和 /lib/systemd/system/ 目錄下存放著這些單元文件。
什么是systemctl
systemctl:管理 systemd 的命令行工具
- 功能:
systemctl 是與 systemd 配合使用的命令行工具,用于啟動、停止、重新啟動、查看、啟用或禁用 systemd 管理的服務。它是用戶與 systemd 交互的主要方式。 - 作用:
啟動和停止服務:通過 systemctl 命令,你可以啟動、停止或重啟任何由 systemd 管理的服務。
查看服務狀態:systemctl status 命令可以用來查看服務的當前狀態,幫助管理員診斷服務是否正常運行。
管理系統:systemctl 也可用于關閉、重啟、掛起系統等操作。
啟用/禁用服務:systemctl enable 用于設置服務開機啟動,systemctl disable 用于禁止服務開機啟動。 - 常見命令示例:
- 啟動服務:systemctl start <service_name>
- 停止服務:systemctl stop <service_name>
- 查看服務狀態:systemctl status <service_name>
- 重啟服務:systemctl restart <service_name>
- 設置服務開機啟動:systemctl enable <service_name>
- 設置服務不開機啟動:systemctl disable <service_name>
關系
- systemd 是基礎,systemctl 是工具:
systemd 是系統和服務的管理器,它負責實際的服務管理、進程監控、資源分配等。而 systemctl 是一個命令行工具,用戶通過它與 systemd 進行交互,執行啟動、停止、查看狀態等操作。
可以理解為,systemd 是背后的系統管理框架,而 systemctl 是用戶與其交互的接口。 - systemctl 控制 systemd:
systemctl 是通過向 systemd 發送指令來管理服務和系統。例如,當你通過 systemctl start <service_name> 啟動一個服務時,systemctl 會告訴 systemd 啟動該服務,systemd 會根據服務的配置文件啟動服務并管理它。
自定義自啟動服務
linux自啟任務詳解
想要自定義一個自啟服務,需要兩個東西:可執行程序(我們自己的后臺業務程序)和systemd的服務腳本
假設我們自己的業務程序名為:test_demo,服務腳本名為:test_demo.service
當然了這個程序僅做演示比較簡單,僅有一個test_demo_main.c文件,代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>const char * filePath = "/home/lijilei/1.txt";
const char * text = "hello world\n";
const char * textend = "end lalala\n";
int g_count = 0;int main(int argc,char**argv)
{FILE *fp = NULL;fp = fopen(filePath,"a+");assert(fp > 0);while(true){sleep(6);fwrite(text, strlen(text),1,fp);fflush(fp); ++g_count;if(g_count > 10){fwrite(textend, strlen(textend),1,fp);break;}}fprintf(fp,"我要寫東西: %s","東西");fflush(fp);fclose(fp);return 0;}
使用cc -o test_demo test_demo_main.c
可編譯出test_demo程序
該演示程序邏輯相當簡單:打開一個文件/home/lijilei/1.txt,向文件中分10次寫入內容,然后退出
test_demo.service文件也相當簡單
#move this file to /etc/systemd/system/
[Unit]
Description=Start up test_demo[Service]
Type=simple
ExecStart=/home/lijilei/xlib_xdnd/test_demo
Restart=on-failure[Install]
WantedBy=multi-user.target
腳本被systemd執行的時候會拉起ExecStart指定路徑下的/home/lijilei/xlib_xdnd/test_demo程序;
將腳本放到/etc/systemd/system/目錄下,按循序執行如下指令:
- sudo systemctl enable test_demo.service 啟用服務,以便在系統啟動時自動啟動
- sudo systemctl start test_demo.service 啟動test_demo.service服務,也就是變相的拉起配置的ExecStart=/home/lijilei/xlib_xdnd/test_demo程序
- sudo systemctl status test_demo.service 停止服務
當修改.service文件后執行
- sudo systemctl daemon-reload 當有修改.service文件時,需重新加載
上述的配置已經可以實現開機自啟一個服務運行
自定義自啟動守護進程
自啟動守護進程的業務場景
在上述自啟服務的基礎上,將業務服務程序改為守護進程程序,使用守護進程去守護目標業務程序會更方便的控制業務程序的生命周期;
比如將守護進程改為看門狗程序,業務程序一直給看門狗發指令(喂狗),當業務程序因為業務崩潰了,則守護進程(看門狗主動拉起)業務程序,當然了我這里不會演示如何寫一個看門狗程序,這里用定時查看進程快照的方式檢測目標業務程序是否在執行,如果不在執行則拉起
什么是守護進程
守護進程是個孤兒進程,它的運行脫離了進程組的管控,無法接受進程退出信號,會一直運行在后臺直到本身發生崩潰退出
為什么使用守護進程
守護進程的特性決定了它不會因為任何退出信號而關閉,所以適合用來執行監控任務,只要守護進程自帶的業務邏輯足夠簡單,那守護進程將永遠運行,直到系統關機,能讓守護進程退出的方法只有三種
- 系統關機
- 找到守護進程的pid,手動kill
- 守護進程因自己的運行bug崩潰退出
因為systemd的功能,我們可以克服第一個方法跟第三個方法導致的守護進程因關機或崩潰而無法再次運行的問題
怎么寫一個守護進程
這里創建一個名為daemond.c的文件,文件內容如下:
// daemon.c
#include <stdio.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <time.h>
#include <syslog.h>
#include <errno.h>
#include <string.h>
#include <assert.h>static FILE *g_fp = NULL;
static time_t g_now;
static const char* PIDFile = "/var/daemond.pid";
//static const char* LOCKDir = "/var/run/daemond";
static const char* LOGFile = "/var/log/daemond.txt";//看護的程序名字,可以是多個
static const char* PROCESSName1 = "test_demo";
static const char* PROCESSName2 = NULL;static int init_daemon(void)
{pid_t pid;int i;pid = fork();if(pid > 0){//第一步,結束父進程,使得子進程成為后臺exit(0);}else if(pid < 0){return -1;}/*第二步建立一個新的進程組,在這個新的進程組中,子進程成為這個進程組的首進程,以使該進程脫離所用終端*/setsid();/*再次新建一個子進程,退出父進程,保證該進程不是進程組長,同時讓該進程無法再打開一個新的終端*/pid = fork();if(pid > 0){exit(0);}else if(pid < 0){return -1;}//第三步:關閉所用從父進程繼承的不再需要的文件描述符for(i = 0;i < NOFILE;close(i++));//第四步:改變工作目錄,使得進程不與任何文件系統聯系chdir("/");//第五步:將文件屏蔽字設置為0umask(0);//第六步:忽略SIGCHLD信號 執行第二步后就不需要執行該步驟signal(SIGCHLD,SIG_IGN);// 1. 忽略其他異常信號// 忽略子進程結束信號,防止產生僵尸進程//signal(SIGCLD, SIG_IGN);// 忽略管道破裂信號,防止程序因向已關閉的管道寫入而異常退出//signal(SIGPIPE, SIG_IGN);// 忽略停止信號,守護進程通常不應被外部信號隨意停止//signal(SIGSTOP, SIG_IGN);return 0;
}static int program_running_number(const char *prog)
{if(prog == NULL) {return 0;}FILE *fp;int count = 0;char buf[8] = {0};char command[128];snprintf(command, sizeof(command), \"ps -ef | grep -v grep | grep -w -c %s", prog);command[sizeof(command) - 1] = '\0';fp = popen(command, "r");if (fp == NULL) {time(&g_now);fprintf(g_fp,"系統時間:\t%s\t\t execute %s failed: %s",ctime(&g_now),command, strerror(errno));fflush(g_fp);return 0;}if (fgets(buf, sizeof(buf), fp)) {count = atoi(buf);}pclose(fp);return count;
}static int createPIDFile(const char* File)
{umask(000);FILE *pidfile = fopen(File, "w");if (pidfile) {fprintf(pidfile, "%d", getpid());fclose(pidfile);return 0;} else {return -1;}
}static int createLOCKDir(const char* dir)
{char cmd[256] = {0};sprintf(cmd,"mkdir %s",dir);int ret = system(cmd);if (ret == 0) {return 0;} else {return -1;}
}static void watchProcess(const char** prcsessList)
{for (const char **prog = prcsessList; *prog; prog++) {if (program_running_number(*prog) > 0) {//fprintf(g_fp,"%s is running.\n", *prog);} else {time(&g_now);fprintf(g_fp,"系統時間:%s %s isn't running.\n",ctime(&g_now),*prog);fflush(g_fp);//再次執行喚起目標程序指令(可替換拉起進程指令)char cmd[256] = {0};sprintf(cmd,"sudo systemctl start %s.service",*prog);fprintf(g_fp,"執行命令: %s\n",cmd);int value = system(cmd);if (value == -1) {time(&g_now);fprintf(g_fp,"系統時間:%s %s : system() failed\n",ctime(&g_now),cmd);fflush(g_fp);} else if (WIFEXITED(value)) {time(&g_now);fprintf(g_fp,"系統時間:%s %s executed successfully with exit code %d: succeed\n",ctime(&g_now),cmd,WEXITSTATUS(value));fflush(g_fp);} else if (WIFSIGNALED(value)) {time(&g_now);fprintf(g_fp,"系統時間:%s %s : terminated by signal %d\n",ctime(&g_now),cmd,WTERMSIG(value));fflush(g_fp);} else {time(&g_now);fprintf(g_fp,"系統時間:%s %s : Unknown status\n",ctime(&g_now),cmd);fflush(g_fp);printf("Unknown status\n");}} }
}int main()
{init_daemon(); createPIDFile(PIDFile);//createLOCKDir(LOCKDir);while(1) {sleep(3);g_fp = fopen(LOGFile,"a+");if(g_fp == NULL) {return -1;}const char *program_name_list[] = {PROCESSName1, PROCESSName2};//這里修改進程看護邏輯watchProcess(program_name_list);fflush(g_fp);fclose(g_fp);}return 0;
}
使用cc -o daemond daemon.c
可編譯出daemond守護進程程序
該daemond邏輯比較簡單,就是負責監視test_demo程序,如果test_demo程序退出了就調用systemctl指令,執行test_demo.service,再次拉起test_demo
daemond.service的寫法就稍微跟test_demo.service不同了
#move this file to /etc/systemd/system/
[Unit]
Description=Start up daemond
After=network.target
[Service]
User=root
Group=root
ExecStart=/home/lijilei/xlib_xdnd/daemond --single-instance
#當進程退出時自動重啟
Restart=always
#適用于后臺運行的服務,systemd 等待父進程退出,并且通過 PID 文件確認進程啟動
Type=forking
#適用于后臺運行的服務,systemd 等待父進程退出,并且通過 PID 文件確認進程啟動
PIDFile=/var/daemond.pid
#只終止主進程,不終止子進程
KillMode=process
#RestartSec=5 #服務崩潰后會等待 5 秒鐘再重啟
#StartLimitIntervalSec=10 #定義了一個 10 秒的時間窗口
#StartLimitBurst=1 #在 10 秒內,服務最多重啟 1 次。如果超過這個次數,systemd 將不會再重啟服務
#刪除PID文件
ExecStopPost=/bin/rm -f /var/daemond.pid
#刪除日志文件
ExecStopPost=/bin/rm -f /var/log/daemond.txt
[Install]
WantedBy=multi-user.target
將腳本放到/etc/systemd/system/目錄下,按順序執行如下指令:
- sudo systemctl enable daemond.service 啟用服務,以便在系統啟動時自動啟動
- sudo systemctl start daemond.service daemond.service服務,也就是變相的拉起配置的/home/lijilei/xlib_xdnd/daemond程序
執行效果
把test_demo.service和daemond.service都加入開機自啟后會出現如下現象:
- test_demo.service會拉起test_demo程序
- test_demo程序在完成打印后退出
- daemond查找進程快照發現test_demo退出,就執行systemctl腳本test_demo.service
- test_demo.service會拉起test_demo程序
- …如此反復執行
查看下daemon.service的執行狀態
$ sudo systemctl status daemond.service ● daemond.service - Start up daemondLoaded: loaded (/etc/systemd/system/daemond.service; enabled; vendor preset: enabled)Active: active (running) since Fri 2024-11-22 01:43:28 UTC; 2 weeks 0 days agoMain PID: 125749 (daemond)Tasks: 1 (limit: 14203)Memory: 13.9MCGroup: /system.slice/daemond.service└─125749 /home/lijilei/xlib_xdnd/daemond --single-instanceWarning: journal has been rotated since unit was started, output may be incomplete.
發現這個服務已經連續運行兩周了
查看下1.txt內容:
發現已經打印了20幾萬行信息了
附錄
如果你在 systemd 單元文件中使用了其他不熟悉或不常見的配置項,建議通過以下命令來驗證服務單元文件的正確性:
- sudo systemd-analyze verify /etc/systemd/system/your_service.service
這個框架有個問題就是daemon在調用system()函數時能執行但是返回值是-1,猜測是由systemctl導致的.后面我再研究研究
以上