linux小技巧:注册为服务,设置开机启动

2,671 阅读10分钟

linux小技巧:注册为服务,设置自启动

「这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战

方法1. System V Init服务

System V Init服务都在目录/etc/init.d/下面。而实际是一个软连接

image.png

软连接连接到/etc/rc.d/init.d/

image.png

运行级别(run level)

Init进程是系统启动之后的第一个用户进程,所以它的pid(进程编号)始终为1。init进程上来首先做的事是去读取/etc/目录下inittab文件中initdefault id值,这个值称为运行级别(run-level)。它决定了系统启动之后运行于什么级别。运行级别决定了系统启动的绝大部分行为和目的。这个级别从0到6 ,具有不同的功能。不同的运行级定义如下:

  • 0 - 停机(千万别把initdefault设置为0,否则系统永远无法启动)
  • 1 - 单用户模式
  • 2 - 多用户,没有 NFS
  • 3 - 完全多用户模式(标准的运行级)
  • 4 - 系统保留的
  • 5 - X11 (x window)
  • 6 - 重新启动 (千万不要把initdefault 设置为6,否则将一直在重启 )

/etc/rc.d/与/etc/rc.d/init.d的关系

/etc/rc.d/init.d这个目录下的脚本就类似与windows中的注册表,在系统启动的时候执行。

在决定了系统启动的run level之后,/etc/rc.d/rc这个脚本先执行。在RH9和FC7的源码中它都是一上来就check_runlevel(),知道了运行级别之后,对于每一个运行级别,在rc.d下都有一个子目录分别是rc0.d,rc1.d ….. rc6.d。

每个目录下都是到init.d目录的一部分脚本一些链接。每个级别要执行哪些服务就在相对应的目录下,比如级别5要启动的服务就都放在rc5.d下,但是放在这个rc5.d下的都是一些链接文件,链接到init.d中相对应的文件,真正干活到init.d里的脚本。

  1. 这些链接文件前面为什么会带一个Kxx或者Sxx呢?
  • K的表示停止(Kill)服务,会自动给脚本带上stop参数
  • S表示开启(Start)服务,会自动给脚本带上start参数
  1. K和S后面带的数字呢?
  • 用来排序,就是决定这些脚本执行的顺序,数值小的先执行,数值大的后执行
  • 很多时候这些执行顺序是很重要的,比如要启动Apache服务,就必须先配置网络接口
  1. 无意中发现同一个服务带S的和带K的链接到init.d之后是同一个脚本
  • S给和K还分别给init.d下面的脚本传递了start和stop的参数。
    • 传S时相当于执行了/etc/rc.d/init.d/xxx start
    • 传K就相当于/etc/rc.d/init.d/xxx stop

实践

1. 创建脚本

vi /app/self-start/myApp.sh

脚本定义了接收系统启动时传过来的参数该如何运行程序。

#!/bin/sh
# chkconfig: 2345 85 15
# description:ljw test auto_run
 
##第一行,告诉系统使用的shell

##第二行,2345代表在设置在那个level中是on的,如果一个都不想on,那就写一个横线"-",85和15, 后面两个数字代表S和K的默认排序号 ,
##告诉chkconfig程序,需要在rc2.d~rc5.d目录下,创建名字为S80myApp的文件连接, 第一个字符是S,系统在启动的时候,运行脚本myApp

##注意上面的三行中,第二,第三行是必须的,否则在运行chkconfig –add auto_run时,会报错。
### 85 数字越小 启动优先级别越高
### 15 数字越小 关闭优先级别越高 
### description描述可以在开机日志中查找:cat /var/log/messages | grep ljw
#程序名
RUN_NAME="myApp"
 
#jar 位置
JAVA_OPTS=/app/self-start/$RUN_NAME.jar
LOG_OPTS=/app/self-start/$RUN_NAME-log.log

echo $JAVA_OPTS
echo $LOG_OPTS
 
#开始方法
start() {

		## 判断是否已经启动
		isRun=`ps -ef | grep $JAVA_OPTS | grep -v grep  | awk '{print $2}'`
		if [ $isRun ]
		then
			echo "$RUN_NAME is running!! Do not operate"
		else
			nohup java -jar $JAVA_OPTS > $LOG_OPTS &
			echo "$RUN_NAME started success!"
		fi
      
}
 
#结束方法
stop() {
        echo "stopping $RUN_NAME ..."
        ####赋值=号左边不能有空格,深坑啊。。。
        aa=`ps -ef | grep $JAVA_OPTS | grep -v grep | grep -v stop | awk '{print $2}'`
        echo kill: $aa
        kill -9 $aa
}
 
case "$1" in
        start)
            start
            ;;
        stop)
            stop
            ;;
        restart)
            stop
            start
            ;;
        *)
                echo "Userage: $0 {start|stop|restart}"
                exit 1
esac

2. 赋予脚本和程序权限

chmod +x /app/self-start/myApp.sh

chmod +x /app/self-start/myApp.jar

3. 在\etc\init.d 下创建脚本软连接到脚本

sudo ln -s /app/self-start/myApp.sh /etc/init.d/myApp

4. 添加为系统服务

chkconfig --add myApp

查看是否添加

chkconfig --list

image.png

5. 设置开机自启动

chkconfig myApp on

6. 服务命令

开启服务

service myApp start

停止服务

service myApp stop

重启服务

service myApp restart

7. 查看服务器重启日志

服务器重启可以重启配置的服务,配置的服务的description描述可以在开机日志中查找。

cat /var/log/messages | grep ljw

报错:服务不支持 chkconfig

请注意检查脚本的前面,是否有完整的两行:

# chkconfig: 2345 85 15
# description:ljw test auto_run

在脚本前面这两行是不能少的,否则不能chkconfig命令会报错误。

这是根据脚本配置的chkconfig自动生成的软连接:2345级别都是S85:开启状态排序85 image.png

如果运行chkconfig老是报错,如果脚本没有问题,我建议,直接在rc0.d~rc6.d下面创建到脚本的文件连接来解决,原理都是一样的。

方法2. Systemd服务(推荐)

Systemd作为后起之秀,功能更加强大,支持的命令和参数也更多,Unit 是 systemd 进行任务管理的基本单位

Systemd中Unit文件的存放目录

  1. /etc/systemd/system:系统或用户自定义的配置文件
  2. /run/systemd/system:软件运行时生成的配置文件
  3. /usr/lib/systemd/system:系统或第三方软件安装时添加的配置文件。(也是lib/systemd/system;centos8中,因为/lib目录已经变成了/usr/lib的一个符号链接)
  • Unit 文件按照 Systemd 约定,被放置指定的三个系统目录之一即可
  • 三个目录是有优先级的越上面的优先级越高
  • 在三个目录中有同名文件的时候,只有优先级最高的目录里的文件会被使用
  • Systemd 默认从目录 /etc/systemd/system/ 读取配置文件,里面存放的大部分文件都是符号链接,指向目录 /usr/lib/systemd/system/
  • /etc/systemd/system目录下通常不放置unit文件本身,而是放置/usr/lib/systemd/system中unit文件的符号链接
# 查看所有单元
$ systemctl list-unit-files

# 查看所有 Service 单元
$ systemctl list-unit-files --type service

# 查看所有 Timer 单元
$ systemctl list-unit-files --type timer

unit的类型

  • Service unit:系统服务
  • Target unit:多个 Unit 构成的一个组
  • Device Unit:硬件设备
  • Mount Unit:文件系统的挂载点
  • Automount Unit:自动挂载点
  • Path Unit:文件或路径
  • Scope Unit:不是由 Systemd 启动的外部进程
  • Slice Unit:进程组
  • Socket Unit:进程间通信的 socket
  • Swap Unit:swap 文件
  • Timer Unit:定时器
  • snapshot Unit:表示由 systemctl snapshot 命令创建的 Systemd Units 运行状态快照

Service unit的文件格式说明

service 类型的 unit 代表一个后台服务进程。接下来我们就详细的介绍如何配置 service 类型的 unit。

下面我们先来看一个简单的服务配置:

cat /usr/lib/systemd/system/firewalld.service

[Unit]
Description=firewalld - dynamic firewall daemon
Before=network-pre.target
Wants=network-pre.target
After=dbus.service
After=polkit.service
Conflicts=iptables.service ip6tables.service ebtables.service ipset.service
Documentation=man:firewalld(1)

[Service]
EnvironmentFile=-/etc/sysconfig/firewalld
ExecStart=/usr/sbin/firewalld --nofork --nopid $FIREWALLD_ARGS
ExecReload=/bin/kill -HUP $MAINPID
# supress to log debug and error output also to /var/log/messages
StandardOutput=null
StandardError=null
Type=dbus
BusName=org.fedoraproject.FirewallD1
KillMode=mixed

[Install]
WantedBy=multi-user.target
Alias=dbus-org.fedoraproject.FirewallD1.service

服务类型的配置文件名称必须以 .service 结尾。

Unit

[Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系,包括在什么服务之后才启动此 unit 之类的设置。

  • Description:关于该unit的简短描述

  • Documentation:文档地址

  • Requires:当前 Unit 依赖的其他 Unit,如果它们没有运行,当前 Unit 会启动失败.(强依赖)

  • Wants:与当前 Unit 配合的其他 Unit,如果它们没有运行,当前 Unit 不会启动失败.(弱依赖)

  • BindsTo:与Requires类似,它指定的 Unit 如果退出,会导致当前 Unit 停止运行

  • Before:表示本服务需要在xxx服务启动之前执行

  • After:表示本服务需要在xxx服务启动之后执行

  • Conflicts:这里指定的 Unit 不能与当前 Unit 同时运行

  • Condition...:当前 Unit 运行必须满足的条件,否则不会运行

  • Assert...:当前 Unit 运行必须满足的条件,否则会报启动失败

所有的启动设置之前,都可以加上一个连词号(-),表示抑制错误,即发生错误的时候,不影响其他命令的执行。

比如,EnvironmentFile=-/etc/sysconfig/sshd(注意等号后面的那个连词号),就表示使/etc/sysconfig/sshd文件不存在,也不会抛出错误。

Service

[Service]区块用来 Service 的配置,只有 Service 类型的 Unit 才有这个区块。

不同的 unit 类型就得要使用相对应的设置项目,比如 timer 类型的 unit 应该是 [Timer];socket 类型的 unit 应该是 [Socket];服务类型的 unit 就是 [Service],这个项目内主要在规范服务启动的脚本、环境配置文件文件名、重新启动的方式等等。

  • Type:定义启动时的进程行为它有以下几种值
    • Type=simple:默认值,执行ExecStart指定的命令,启动主进程,ExecStart字段启动的进程为主进程
    • Type=forking:以 fork 方式从父进程创建子进程,创建后父进程会立即退出,子进程将作为该服务的主进程继续运行,这是传统UNIX守护进程的经典做法。(jar服务用这个)
    • Type=oneshot:一次性进程,Systemd 会等它执行完,才继续启动其他服务
    • Type=dbus:类似于simple,但会等待 D-Bus 信号后启动
    • Type=notify:当前服务启动完毕,会通知Systemd,再继续往下执行
    • Type=idle:类似于simple,但是要等到其他任务都执行完,才会启动该服务。一种使用场合是为让该服务的输出,不与其他服务的输出相混合
  • User :使用哪个用户环境变量来正确运行该服务
  • ExecStart:启动当前服务的命令:不能接受 <, >, >>, |, & 等特殊字符,很多的 bash 语法也不支持。所以,要使用这些特殊的字符时,最好直接写入到脚本里面去!
  • ExecStartPre:启动当前服务之前执行的命令
  • ExecStartPost:启动当前服务之后执行的命令
  • ExecReload:重启当前服务时执行的命令
  • ExecStop:停止当前服务时执行的命令
  • ExecStopPost:停止当其服务之后执行的命令
  • RestartSec:自动重启当前服务间隔的秒数
  • Restart:定义何种情况 Systemd 会自动重启当前服务
    • no(默认值):退出后不会重启
    • on-success: 只有正常退出时(退出状态码为0),才会重启
      • 正常退出:退出码为"0",或者进程收到 SIGHUP, SIGINT, SIGTERM, SIGPIPE 信号并且退出码符合 SuccessExitStatus= 的设置。
    • on-failure:非正常退出时(退出状态码非0),包括被信号终止和超时,才会重启
    • on-abnormal:只有被信号终止和超时,才会重启
    • on-abort:只有在收到没有捕捉到的信号终止时,才会重启
    • on-watchdog: 超时退出,才会重启
    • always: 不管是什么退出原因,总是重启(当进程是由于 systemd 的正常操作(例如 systemctl stop|restart)而被停止时, 该服务不会被重新启动。即使用kill -9还是会重启的

image.png

  • TimeoutSec:定义 Systemd 停止当前服务之前等待的秒数
  • SuccessExitStatus :定义附加的进程"正常退出"状态,如:SuccessExitStatus=1 2 8 SIGKILL ;表示当进程的退出码是 1, 2, 8 或被 SIGKILL 信号终止时,都可以视为"正常退出"
  • PrivateTmp: 否使用私有的tmp目录
  • KillMode:定义 Systemd 如何停止服务,可以设置的值如下:
    • control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
    • process:只杀主进程
    • mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
    • none:没有进程会被杀掉,只是执行服务的 stop 命令
  • Environment:指定环境变量,可以使用多次
[Service]
# Client Env Vars
Environment=XXX_A=A
Environment=XXX_B=B
  • EnvironmentFile:环境参数文件
    • 1.脚本 可以把下面的内容保存到文件 testenv 中:
      • AAA_IPV4_ANCHOR_0=X.X.X.X
    • 2.声明环境文件 EnvironmentFile=/testenv
    • 3.使用:ExecStart=/xxx xx${AAA_IPV4_ANCHOR_0}

Install

[Install]通常是配置文件的最后一个区块,用来定义如何启动,以及是否开机启动,这个部分主要设置把该 unit 安装到哪个target。

  • WantedBy:它的值是一个或多个 Target,当前 Unit 激活时(enable)符号链接会放入/etc/systemd/system目录下面以 Target 名 + .wants后缀构成的子目录中(弱依赖),即是等待xxx的全部内容就绪就安装启动
  • RequiredBy:它的值是一个或多个 Target,当前 Unit 激活时,符号链接会放入/etc/systemd/system目录下面以 Target 名 + .required后缀构成的子目录中(强依赖)
  • Alias:当前 Unit 可用于启动的别名
  • Also:当前 Unit 激活(enable)时,会被同时激活的其他 Unit

systemd.service 中文手册

Service Unit实践

1. 在/etc/systemd/system/创建Unit脚本

查看root用户所在的组,以及组内成员 groups root

创建服务脚本:vi /etc/systemd/system/myService.service

注:这里不能用软连接。直接在此路径下创建service脚本。因为取消自启动命令会把软连接一并删除的。到时又要手动创建软连接才能用,麻烦。

jar服务输入下面内容:

[Unit]
Description=My Spring Boot Service
After=syslog.target

[Service]
##类型为fork子线程执行,不然服务会重复重启到最后报错
Type=forking
#true:tmp共享可使用jps命令查看java进程;false:私有,只能用ps -ef|grep查看
#PrivateTmp=true
User=root
Group=root
##这里只配置开启命令就行。关闭和重启systemd会帮我控制,配置也是可以的。
ExecStart=/app/self-start/start.sh

### 可以使用方法1的上面myApp脚本,配置开启,重启,关闭执行的命令
#ExecStart=/app/self-start/myApp.sh start
#ExecReload=/app/self-start/myApp.sh restart
#ExecStop=/app/self-start/myApp.sh stop
Restart=always
#RestartPreventExitStatus=1

[Install] 
WantedBy=multi-user.target

注意:

  • PrivateTmp=true
    • 服务启动时会在/tmp/目录下生成类似:/tmp/systemd-private-bceaa1ad35764ee2aea3cdd52fab89cd-myService.service-rVFD0q/tmp/hsperfdata_root的文件夹,用于存放myService的临时文件。存放了线程的pid-私有的,只能对自己账号有操作权限
    • jps命令使查询/tmp/目录下得共享文件,所有配置了PrivateTmp=true的进程用该命令使看不到的。请使用:ps -ef|grep java
  • PrivateTmp=false
    • 服务启动时会在/tmp/目录下生成共享文件,存放了线程的pid-所有用户共有读写权限。
  • multi-user.target:等待多用户系统的全部内容就绪才安装启动

2. 创建启动脚本

vi /app/self-start/start.sh

启动服务并把进程的pid保存一份到自定义的文件/app/self-start/myApp.pid

#!/bin/sh
nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/myApp.log & echo $! >/app/self-start/myApp.pid

3. 赋予脚本和程序权限

chmod +x /app/self-start/start.sh

chmod +x /app/self-start/myApp.jar

4. 重新加载所有修改过的unit文件

systemctl daemon-reload

5.查看配置

能查看到配置证明服务文件没有语法错误,逻辑和环境等错误查不到,要看启动日志

systemctl show -p ExecStart myService

6.启动服务并配置开机启动

启动:sudo systemctl start myService

状态:sudo systemctl status myService

停止:sudo systemctl stop myService

重启:sudo systemctl restart myService

设置开机启动:sudo systemctl enable myService

取消开机启动:sudo systemctl disable myService

查看服务启动日志(问题排查):journalctl -u myService.service

Timer Unit实践

Timer unit的详细配置

Timer 类型的 unit 主要用来执行定时任务,并有可能取代 cron 服务。

与服务类型的 unit 不同,timer unit 配置文件中的主要部分是 [Timer],下面是其主要的配置项:

[Timer]

  • OnActiveSec:定时器生效后,多少时间开始执行任务
  • OnBootSec:系统启动后,多少时间开始执行任务
  • OnStartupSec:Systemd 进程启动后,多少时间开始执行任务
  • OnUnitActiveSec:该单元上次执行后,等多少时间再次执行
  • OnUnitInactiveSec: 定时器上次关闭后多少时间,再次执行
  • OnCalendar:基于绝对时间,而不是相对时间执行
  • AccuracySec:如果因为各种原因,任务必须推迟执行,推迟的最大秒数,默认是60秒
  • Unit:真正要执行的任务,默认是同名的带有.service后缀的单元
  • Persistent:如果设置了该字段,即使定时器到时没有启动,也会自动执行相应的单元
  • WakeSystem:如果系统休眠,是否自动唤醒系统

1. 创建备份脚本

vi /app/self-start/backup.sh

#!/bin/bash
mydate()
{
        date "+%Y%m%d%H%M%S"
}
backupdate=$(mydate)
## 打包需要备份的文件夹
tar -zcPf /app/self-start/bk.${backupdate}.tar.gz /app/self-start/bk

2. 赋予执行权限

sudo chmod +x /app/self-start/backup.sh

3. service unit 配置文件

vi /etc/systemd/system/backup.service

[Unit]
Description=my backup learn dir service

[Service]
User=root
Group=root
Type=simple
## 执行该脚本
ExecStart=/app/self-start/backup.sh

[Install]
WantedBy=multi-user.target

4. 配置定时服务

vi /etc/systemd/system/backup.timer

[Unit]
Description=my backup learn dir timer

[Timer]
##每隔15分钟执行
#OnCalendar=*:0/15
#每月26号的凌晨0点半执行一次
#OnCalendar=*-*-26 00:30:00

#自定时器启动1分钟后间隔1秒执行一次
OnBootSec=1min
OnUnitActiveSec=30s

Persistent=true
## 配置要定时的服务
Unit=backup.service

[Install]
WantedBy=multi-user.target

5. 相关命令

刷新配置:sudo systemctl daemon-reload

开启备份服务: sudo systemctl start backup.service

开启定时服务:sudo systemctl start backup.timer

开机启动备份服务 : sudo systemctl enable backup.service

开机启动定时服务 : sudo systemctl enable backup.timer

查看状态备份服务: sudo systemctl status backup.service

查看状态定时服务: sudo systemctl status backup.timer

查看整个日志 :sudo journalctl

查看 backup.timer 的日志 :sudo journalctl -u backup.timer

查看 backup.timer 和 backup.service 的日志: sudo journalctl -u backup

从结尾开始查看最新日志:sudo journalctl -f

从结尾开始查看 backup.timer 的日志: journalctl -f -u backup.timer

方法3:直接开机启动配置

除了上面的注册服务并设置为开机自启动,还有一个方法就是直接配置开机自启动。

实践

1. 创建脚本

vi /app/self-start/directStart.sh

#!/bin/bash

##可以等待20秒再运行。等待其他服务或环境
##sleep 10
##这时一条test命令
echo "启动了" > /app/self-start/a.txt
##使用绝对路径的java命令。因为服务器重启相关环境可能还没准备好
nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &

2. 赋予脚本和程序权限

chmod +x /app/self-start/myApp.jar

chmod +x /app/self-start/directStart.sh

/etc/rc.local默认是没有执行权限的,要加上才能开机运行。

chmod +x /etc/rc.local

image.png

3. 修改开机启动文件/etc/rc.local

最好把命令绝对路径/bin/bash写全

/bin/bash /app/self-start/directStart.sh 

4. 检查是否配置正常

手动运行测试是否可以。

bash /etc/rc.local

5. 重启测试

reboot

问题

  • 在linux下,如需添加随系统启动而自动运行的服务,只需在/etc/rc.local 脚本文件中添加即可。

  • 但是遇到一个问题是脚本手动测试能正常运行,放在/etc/rc.local中java命令没有正常运行或者执行失败。touch命令能成功。脚本中的命令不带路径的如下:

    • nohup java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &
  • 在系统重启执行这些命令时将报错,无法正常执行!究其原因:

    • 由于在执行rc.local脚本时,PATH环境变量未全部初始化,需在执行/etc/profile 后才被添加到环境变量PATH中。
  • 为了在开机启动时执行,部分命令需要使用绝对路径

[root@k8s2 self-start]# which java
/usr/bin/java
  • nohup /usr/bin/java -jar /app/self-start/myApp.jar > /app/self-start/nohup.out 2>&1 &
  • 这和注册成服务相比是一个比较大的缺点,不能控制脚本启动时机的细腻度和需要什么样的环境。