Linux环境设置用户自定义应用的开机自启动
内容概要
〇、背景
做 Linux 嵌入式开发,经常会遇到要设置自己的应用程序在系统开机的时候自动启动,并且一般情况我们不想安装一些额外的软件去实现此种需求。那么直接根据当前系统自带的默认的一些工具实现用户软件的开机自启动。
本次测验的系统环境为 Ubuntu 16.04 LTS,测试内容均得到正确的验证,如果其他环境出现不一致的现象,请查阅相关的差异。
比如先要实现下面的应用程序的开机自启动(实现的代码最下面给出,有需要的可以查看):
1. 应用程序的名称:app.bin
2. 应用程序的保存路径:/home/ubuntu/app/ 下
3. 应用程序的工作路径:/home/ubuntu/app/ 下
一、使用脚本方式开机自启动
此种方式一般比较简单实用,不需要复杂的指令即可实现。
1.1 开机启动时自动运行程序(未登陆)
在嵌入式系统中,此种方式一般较为常见,系统在启动之后一般是不需要登陆的。
首先,需要说明的是,Linux 系统加载后,运行第一个用户级进程 init。然后 init 根据配置文件继续引导启动过程,启动其它进程。在目录 /etc/ 目录下,有 rc0.d - rc6.d 各启动级别的启动脚本的目录,有 rcS.d 单用户模式启动脚本, 也有 rc.local,此脚本里面基本没有内容,其实就是一个放开机自启动程序的模板。将需要启动的程序写在 exit 0 行的前面即可。
所以,对于上述的自启动的要求,那么此时脚本我们这样写来实现。
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# 启动应用程序,因为程序是无限循环,所以一般是用后台的形式形式启动
./home/ubuntu/app/app.bin &
exit 0
或者如下面这种方式:
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# 进入到应用程序所在的目录,绝对路径
cd /home/ubuntu/app/
# 启动应用程序,因为程序是无限循环,所以一般是用后台的形式形式启动
./app.bin &
exit 0
对于上述两种两种不同的启动方式,通过下面的图2可以看出来,其实就是程序的工作的路径不同而已,所以也就是说,如果应用程序对于工作目录又要求,可以用上述的第二种方式进行启动。
1.2 用户登录时自动运行程序
对于特殊的有些应用程序,可能需要在某些用户的登录的才自启动,比如非管理员账号时需要进行操作监督或者日志记录等功能,那么就需要在用户登录的才启动,其他情况下不则启动,免得浪费资源。
当登入系统获得一个shell进程时,其读取环境设定有三步,分别如下描述。
首先读入的是系统全局环境变量设定的文件 /etc/profile,然后根据其内容读取额外的设定的文档,如 /etc/profile.d 和 /etc/inputrc ,需要注意的是 /etc/profile 中设定的变量(全局)的可以作用于任何用户;
然后根据不同的用户帐号,在其 /home/ 目录读取 ~/.bash_profile,如果读取失败则读取 ~/.bash_login,如果还读取失败则再读取 ~/.profile,这三个文件设定基本上是一样的,读取有优先关系,且只执行最先找到的一个;
然后在根据用户帐号读取 ~/.bashrc,此文件在每次 shell script 的执行都会读取一次。
一般情况下,如果没有进行过相关的设置,则配置文件 ~/.bash_profile 和 ~/.bash_login 是默认不存在,所以直接在 ~/.profile 文件中新增如下内容,即可完成登录之后的程序启动。
./app/app.bin &
或者如果需要指定工作目录,则使用下面指令。
cd /home/ubuntu/app/
./app.bin &
二、使用Systemd添加开机自启动服务
Systemd 是系统启动和服务器守护进程管理器,负责在系统启动或运行时,激活系统资源,服务器进程和其它进程。
对于Systemd详细信息,可以查看下面内容:
Fedora wiki: Systemd/zh-cn:https://fedoraproject.org/wiki/Systemd/zh-cn
systemd.service 官方手册:https://www.freedesktop.org/software/systemd/man/systemd.service.html
systemd.service 中文翻译:http://www.jinbuguo.com/systemd/systemd.service.html#
一般情况下,systemd 的配置文件主要放在 /usr/lib/systemd/system/,或者 /etc/systemd/system/。不过 unit 文件在 /etc/systemd/system 下的优先级要高于 /usr/lib/systemd/system 下的。所以,我们需要将编写好的配置文件放置任一个目录中即可。
2.1 系统级开机自启动
2.1.1 配置文件编写
通常情况下,systemd默认为系统及服务。所以我们简单的了解了systemd的相关知识之后,对于上面的应用程序启动的要求,我们可以轻轻松松的写出他的配置文件,名称就为 app.service,内容如下。
# 启动顺序与依赖关系
[Unit]
Description=My app Startup Test Service
After=network.target
# 启动行为
[Service]
Type=simple
WorkingDirectory=/home/ubuntu/app/
ExecStart=/home/ubuntu/app/app.bin
Restart=always
RestartSec=0s
KillMode=mixed
# 定义如何安装配置文件
[Install]
WantedBy=multi-user.target
首先将 app.service 文件复制到 /etc/systemd/system/ 或者 /usr/lib/systemd/system/(如果在 /usr/lib/systemd/ 下没有system目录,则手动创建一个即可)。
添加或修改配置文件后,需要重新加载,使用指令:sudo systemctl daemon-reload,sudo 可省,会自动弹出输出密码的对话框。
设置允许自启动,使用指令:sudo systemctl enable app.service(后缀.service可以省略)
至此,自定义的应用程序可以在下次开机的时候自动启动了,本例中简要测试以下几个指令,具体如下如图3所示。
- 启动:
sudo systemctl start app.service
- 激活开机启动:
sudo systemctl enable app.service
- 取消开机启动:
sudo systemctl disable app.service
- 启动服务:
sudo systemctl start app.service
- 停止服务:
sudo systemctl stop app.service
- 重启服务:
sudo systemctl restart app.service
- 重载服务:
sudo systemctl daemon-reload
- 察看运行情况:
sudo systemctl status app.service
- 查看所有已启动的服务:
sudo systemctl list-units --type=service
2.1.2 配置文件说明
在system目录下,每个服务文件以 .service 结尾, 封装了一个被 systemd 监视与控制的进程。一般文件内容会分为 3个 小节,分别为:
- [Unit] 小节:此区块为启动顺序与依赖关系的描述,包含与单元类型无关的通用信息
- [Service] 小节:描述启动行为,定义如何启动此服务,此部分不可省略
- [Install] 小节:包含单元的启用信息,定义如何安装配置文件,只有 enable 与 disable 命令在启用/停用单元时才会使用此小节
下面就常用的一些配置进行简要的说明,以满足有不通情况下的配置文件的适配。
I. [Unit] 小节
Description=
有利于人类阅读的、对单元进行简单描述的字符串。将被 systemd 或其他程序用来标记此单元,这个字符串应该只用于识别此单元即可,不需要过分说明。Wants=
设置此单元所必须依赖的其他单元。当此单元被启动时,所有这里列出的其他单元只是尽可能被启动。 但是,即使某些单元不存在或者未能启动成功, 也不会影响此单元的启动。 推荐使用此选项来设置单元之间的依赖关系。Before=, After=
强制指定单元之间的先后顺序,接受一个空格分隔的单元列表。不涉及依赖关系。
II. [Service] 小节
Type=
设置进程的启动类型。必须设为 simple, exec, forking, oneshot, dbus, notify, idle 之一。
如果设为 simple, 那么 ExecStart= 字段启动的进程就是该服务的主进程, 并且 systemd 会认为在创建了该服务的主服务进程之后,该服务就已经启动完成。需要注意的是:对于 simple 类型的服务来说,即使不能成功调用主服务进程(例如 User=不存在、或者二进制可执行文件不存在), systemctl start 也仍然会执行成功。
如果设为 forking ,那么表示 ExecStart= 进程将会在启动过程中使用 fork() 系统调用。systemd 会认为在父进程退出之后,该服务就已经启动完成。ExecStart=
在启动该服务时需要执行的 “命令行(命令+参数)”,命令行必须以一个可执行文件(要么是绝对路径、要么是不含任何斜线的文件名)开始, 并且其后的那些参数将依次作为 “argv[1] argv[2] …” 传递给被执行的进程。Restart=
当服务进程 正常退出、异常退出、被杀死、超时等情况的时候,是否或者如何重新启动该服务,可选的选项如下所示。no(默认值):退出后不会重启
on-success:只有正常退出时,才会重启
on-failure:非正常退出时,才会重启
on-abnormal:只有被信号终止和超时,才会重启
on-abort:只有在收到没有捕捉到的信号终止时,才会重启
on-watchdog:超时退出,才会重启
always:不管是什么退出原因,总是重启
RestartSec=
设置在重启服务(Restart=)前暂停多长时间。 默认值是100毫秒(100ms)。 如果未指定时间单位,那么将视为以秒为单位。 例如设为“20"等价于设为"20s”。KillMode=
定义 Systemd 如何停止服务,可选的设置选项如下所示。control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
process:只杀主进程
mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
none:没有进程会被杀掉,只是执行服务的 stop 命令
此小节还有其他的一些常用配置项,下面进行简要的说明。
- User=、Group=:设置启动的用户、组
- WorkingDirectory=:设置服务的工作路径
- ExecReload=:重启服务时执行的命令
- ExecStop=:停止服务时执行的命令
- ExecStartPre=:启动服务之前执行的命令
- ExecStartPost=:启动服务之后执行的命令
- ExecStopPost=:停止服务之后执行的命令
III. [Install] 小节
Alias=
启用时使用的别名,可以设为一个空格分隔的别名列表。WantedBy=, RequiredBy=
表示该服务所在的 Target(服务组),表示在使用 systemctl enable 启用此单元时,将会在每个列表单元的 .wants/ 或 .requires/ 目录中创建一个指向该单元文件的软连接。
2.2、用户级开机自启动
systemd有系统和用户区分,只是存放的路径不同,一般情况下:
系统路径:/etc/systemd/system/ 、 /usr/lib/systemd/system/
用户路径:/usr/lib/systemd/user/ 、 /etc/systemd/user/ 、 ~/.confg/systemd/user、 ~/.share/systemd/user/ 等
systemd支持普通用户定义的 unit[s] 开机启动,其操作命令与方式与上述系统级的一样,只是在命令中需要显式的新增 –user 参数且不可省略(省略后即为默认 –system)。所以,其命令形式如下所示。
- 启动:
systemctl --user start app.service
- 激活开机启动:
systemctl --user enable app.service
- 取消开机启动:
systemctl --user disable app.service
- 启动服务:
systemctl --user start app.service
- 停止服务:
systemctl --user stop app.service
- 重启服务:
systemctl --user restart app.service
- 重载服务:
systemctl --user daemon-reload
- 察看运行情况:
systemctl --user status app.service
相较于系统级的方式,用户定义需要说明的主要在于下面的几个方面:
- 用户级需要显式增加参数 –user
- 用户级运行环境用 default.target,系统级通常用 multi-user.target
- 用户级 unit 与系统级 unit 相互独立
- 用户级服务在特定用户登录时才启动,且在系统重启之前将不再自动启动
综上所述,那么用户级的配置文件则可以这样写。
# 启动顺序与依赖关系
[Unit]
Description=My app Startup Test Service
After=network.target
# 启动行为
[Service]
Type=simple
WorkingDirectory=/home/ubuntu/app/
ExecStart=/home/ubuntu/app/app.bin
Restart=always
RestartSec=0s
KillMode=mixed
# 定义如何安装配置文件
[Install]
WantedBy=default.target
法术灵不灵,一试便知,本例中简要测试以下几个指令,具体如下如图4所示。
启动服务之后,服务正常运行,效果图还是上面图 1的那样,重启再试,选择用户登录之后依旧入所料正常启动。
如果作为用户级的服务程序,在配置中还指定了用户名,那么如果此用户名和当前登录用户户名不一致的时候(如下图5所示),此服务将不自动启动,包括root用户。
2.3、开机自启动另一法
1. 首先新建一个启动程序的脚本文件,使用指令 $ vi app-startup.sh
创建并打开。
2. 然后在脚本中写入程序启动的指令,文件中所有内容如下。
#!/bin/bash
### BEGIN INIT INFO
# Provides: can-startup-scrip
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start daemon at boot time
# Description: Enable service provided by daemon.
### END INIT INFO
/home/ubuntu/app/app.bin &
exit 0
3. 使用指令 $ sudo cp app-startup.sh /etc/init.d
将此脚本文件复制到 /etc/init.d/ 目录下。
4. 使用指令 $ update-rc.d app-startup.sh defaults
将此脚本文件执行添加启动服务中。
5. 使用指令 $ service can-startup start
启动服务。
那么以后开机的话此程序也将随机启动。如果想要停止随机启动,也可使用 systemctl、service
等指令进行控制。
三、测试示例源码
#ifndef _GNU_SOURCE
#define _GNU_SOURCE // for get_current_dir_name
#endif
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
const char *ipaddr = "192.168.22.10";
int src_port = 10000;
int dst_port = 20000;
unsigned int cycle_cnt = 0;
char current_msg[128];
int sockfd = 0;
struct sockaddr_in sockaddr;
char *path = get_current_dir_name();
sprintf(current_msg, "%s/app.log", path);
FILE* pfile = fopen(current_msg, "a+");
if(pfile == NULL)
{
exit(-1);
}
/* 设置源端口信息 */
memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = inet_addr(ipaddr);
sockaddr.sin_port = htons(src_port);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bind(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
/* 设置目标端口信息 */
sockaddr.sin_port = htons(dst_port);
while (1)
{
cycle_cnt++;
sprintf(current_msg, "%s, %d\n", path, cycle_cnt);
fwrite(current_msg, strlen(current_msg), 1, pfile);
fflush(pfile);
/* 将数据发送出去 */
sendto(sockfd, current_msg, strlen(current_msg), 0, (const struct sockaddr *)&sockaddr, sizeof(sockaddr));
sleep(1);
}
return 0;
}
好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。
更多推荐
所有评论(0)