系统服务
书接上文: 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("/");
//第五步:将文件屏蔽字设置为0
umask(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 daemond
Loaded: 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 ago
Main PID: 125749 (daemond)
Tasks: 1 (limit: 14203)
Memory: 13.9M
CGroup: /system.slice/daemond.service
└─125749 /home/lijilei/xlib_xdnd/daemond --single-instance
Warning: 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导致的.后面我再研究研究
以上