应用启动的原理
# 目录介绍
- 1.脚本启动思想
- 1.1 程序执行解耦
- 1.2 遇到一个疑问
- 1.3 脚本的职责
- 1.4 脚本设计思想
- 1.5 完整生命周期
- 2.脚本启动原理
- 2.1 启动脚本内容
- 2.2 脚本核心分析
- 2.3 完整执行流程
- 2.4 应用的启动
- 3.脚本关闭原理
- 3.1 关闭脚本内容
- 3.2 脚本核心分析
# 1.脚本启动思想
# 1.1 程序执行解耦
核心思想:将环境准备与程序执行解耦
程序本身只关心业务逻辑,而运行环境的配置(库路径、工作目录、进程管理)交给脚本处理。这样同一个二进制可以在不同环境(开发机、设备、CI)通过不同脚本适配运行。
# 1.2 遇到一个疑问
为什么不直接 ./iotservice?直接执行会面临三个问题:
问题1:找不到动态库
./iotservice
→ error: libboost_system.so.1.82.0: cannot open shared object file
问题2:重复启动
./iotservice &
./iotservice & ← 两个实例抢资源,数据损坏
问题3:进程管理
如何停止?kill 几号进程?崩溃后谁来重启?
2
3
4
5
6
7
8
9
10
脚本就是解决这三个问题的胶水层。
# 1.3 脚本的职责
脚本做了什么
┌──────────────────────────────────────────────┐
│ 脚本的职责 │
├──────────────────────────────────────────────┤
│ 1. 配置运行环境 │
│ export LD_LIBRARY_PATH=./lib │
│ (告诉 OS 去哪里找 .so 动态库) │
│ │
│ 2. 进程管控 │
│ start-stop-daemon 防止重复启动 │
│ PID 文件记录进程号,方便后续 stop │
│ │
│ 3. 统一入口 │
│ 不管谁来启动(人、系统开机脚本、看门狗), │
│ 都走同一个 start.sh,行为一致 │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.4 脚本设计思想
| 原则 | 体现 |
|---|---|
| 环境与程序分离 | 程序不硬编码库路径,由脚本通过 LD_LIBRARY_PATH 注入 |
| 单实例保证 | 脚本层 start-stop-daemon + 程序层 PID 文件锁,双重防护 |
| 可运维性 | start/stop 成对出现,运维人员不需要知道程序内部细节 |
| 可移植性 | 换一台设备,只需调整脚本中的路径,二进制不变 |
本质上,这就是 Linux 世界里服务管理的基本范式——和 systemd、init.d、supervisord 的思想完全一致,只是用最轻量的 shell 脚本实现了相同的事情:配环境 → 防重复 → 拉起进程 → 记录 PID → 用 PID 停止。
# 1.5 完整生命周期
┌─────────────────────────────────────────────────────────────┐
│ start.sh │
│ export LD_LIBRARY_PATH → start-stop-daemon -S iotservice │
└────────────────────────────┬────────────────────────────────┘
│ fork+exec
▼
┌─────────────────────────────────────────────────────────────┐
│ iotservice 进程 │
│ │
│ ① ParseArgs (--env, --signal-parent, ...) │
│ ② RunGuardCheck │
│ open(/tmp/iotservice.pid) → lockf() → write(PID) │
│ ③ 注册信号: SIGINT, SIGTERM → HandleSignal │
│ ④ 初始化模块 → 进入 asio 事件循环 │
│ ↑ │
│ │ 持续运行... │
│ │ │
└─────────────────────────────────────┬───────────────────────┘
│
┌─────────────────────────────────────┼───────────────────────┐
│ stop.sh │ │
│ cat /tmp/iotservice.pid → kill PID ┘ │
│ pkill -f iotservice (兜底) │ │
└─────────────────────────────────────┼───────────────────────┘
│ SIGTERM
▼
┌─────────────────────────────────────────────────────────────┐
│ HandleSignal │
│ ① remove(SIGTERM) // 恢复默认处理 │
│ ② raise(SIGTERM) // 重发信号 → 进程终止 │
│ │
│ OS 回收进程 → 自动释放 PID 文件锁 │
└─────────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 2.脚本启动原理
# 2.1 启动脚本内容
# 指定解释器。用 `/bin/sh` 而非 `/bin/bash`,因为嵌入式设备(RV1126)上通常只有 BusyBox 的 `sh`,没有完整 bash。
#!/bin/sh
# **遇错即停**。任何命令返回非零状态时脚本立即退出,避免前面步骤失败了还继续执行后面的启动命令。
set -e
echo "start iotservice"
# 路径定位(核心之一)这样无论从哪个目录调用 `start.sh`,都能正确定位到应用根目录。
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
# iotservice 依赖的 `.so`(boost、protobuf、openssl 等)都在 `lib/` 下,不在系统默认搜索路径中。`LD_LIBRARY_PATH` 是 Linux 动态链接器的环境变量,告诉它优先去这个目录找共享库。
export LD_LIBRARY_PATH="${SCRIPT_DIR}/lib:${LD_LIBRARY_PATH}"
# 拼出可执行文件的绝对路径:`/data/iotservice/iotservice`。
APP_BINARY="${SCRIPT_DIR}/iotservice"
echo "App directory: ${SCRIPT_DIR}"
echo "Library path: ${LD_LIBRARY_PATH}"
echo "Starting: ${APP_BINARY}"
start-stop-daemon -S -b -q --exec "${APP_BINARY}" &
echo "Process restarted successfully."
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
部署目录结构(install 后)
/data/iotservice/ ← SCRIPT_DIR 的上一级
├── iotservice ← 可执行二进制
├── lib/ ← 动态库(boost、protobuf 等)
├── scripts/
│ ├── start.sh
│ └── stop.sh
├── certs/
└── res/
2
3
4
5
6
7
8
# 2.2 脚本核心分析
设置动态库搜索路径:export LD_LIBRARY_PATH="${SCRIPT_DIR}/lib:${LD_LIBRARY_PATH}"非常重要
进程启动 → ld-linux.so(动态链接器)
→ 读 ELF 中的 DT_NEEDED(需要 libboost_system.so 等)
→ 按 LD_LIBRARY_PATH → /lib → /usr/lib 顺序搜索
→ 找到并加载 → 符号解析 → 进入 main()
2
3
4
不设这个变量 → 链接器找不到库 → 启动失败。
通过 start-stop-daemon 启动进程:start-stop-daemon -S -b -q --exec "${APP_BINARY}" &
| 参数 | 含义 |
|---|---|
-S | Start 模式 |
-b | Background,将进程放到后台运行 |
-q | Quiet,不输出冗余信息 |
--exec | 指定要启动的二进制路径 |
& | 脚本本身不等待 start-stop-daemon 返回 |
start-stop-daemon 的核心能力:
启动前会扫描 /proc,检查是否已有相同 --exec 路径的进程在运行:
- 已运行 → 不做任何事,直接返回(幂等性)
- 未运行 → fork 子进程执行二进制
start-stop-daemon -S --exec /data/iotservice/iotservice
│
├─ 扫描 /proc/*/exe → 发现已有同路径进程
│ → 跳过,返回 0(幂等)
│
└─ 没有发现
→ fork() → execve("/data/iotservice/iotservice")
→ 子进程成为 iotservice,继承 LD_LIBRARY_PATH
2
3
4
5
6
7
8
# 2.3 完整执行流程
start.sh 做了 3 件事:定位自己 → 配置库路径 → 通过 start-stop-daemon 幂等地后台启动进程。核心原理就是用 LD_LIBRARY_PATH 解决动态库加载,用 start-stop-daemon 解决进程管理,让脚本成为二进制和运行环境之间的桥梁。
start.sh 执行
│
├─ ① set -e ← 遇错即停
├─ ② 定位应用根目录 ← 解决"从哪里运行都能找到自己"
├─ ③ export LD_LIBRARY_PATH ← 解决"找不到 .so"
└─ ④ start-stop-daemon ← 解决"防重复启动 + 后台运行"
│
└─ fork+exec → iotservice 进程
│
├─ RunGuardCheck → PID 文件锁(程序内部的二次防护)
├─ 注册 SIGINT/SIGTERM 信号
└─ 进入 asio 事件循环
2
3
4
5
6
7
8
9
10
11
12
# 2.4 应用的启动
应用内部初始化(Application::Run)
main()
→ Application::Run(argc, argv)
│
├─ ① Setup: 解析命令行参数(--env, --version 等)
│
├─ ② RunGuardCheck: 单实例保护
│ open("/tmp/iotservice.pid")
│ lockf(pid_file, F_TLOCK) ← 尝试加文件锁
│ ├─ 成功 → 写入当前 PID → 继续启动
│ └─ 失败 → 说明已有实例在运行 → 退出
│
└─ ③ RunMainThread: 进入主事件循环
│
├─ 注册信号处理: SIGINT + SIGTERM
│ signals->async_wait(HandleSignal)
│
├─ 初始化各模块: Logger → Threads → Device → PaasShadow
│
└─ 进入 asio::io_context 事件循环(阻塞,直到收到退出信号)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
单实例保护的原理(RunGuardCheck):
int pid_file = ::open("/tmp/iotservice.pid", O_CREAT | O_RDWR, 0660);
if (::lockf(pid_file, F_TLOCK, 0) < 0) {
// 文件锁被占用 → 另一个实例正在运行 → 退出
return false;
}
// 锁成功 → 写入当前 PID
::write(pid_file, pid.c_str(), pid.length());
// 故意不关闭文件 → 进程退出时 OS 自动释放锁
2
3
4
5
6
7
8
这比单纯检查 PID 文件是否存在更可靠——进程崩溃后 OS 会自动释放文件锁,不会留下"僵尸" PID 文件导致无法重启。
# 3.脚本关闭原理
# 3.1 关闭脚本内容
#!/bin/sh
# — 遇错即停,但每个 kill 命令都加了 `|| true`,所以不会因为"进程已不存在"而退出脚本。
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
APP_NAME="iotservice"
PID_FILE="/tmp/${APP_NAME}.pid"
echo "stop iotservice"
# 方法1: 通过 PID 文件停止
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
echo "Stopping process with PID $pid"
kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
# 方法2: 通过进程名停止(备用方案)
echo "Checking for remaining ${APP_NAME} processes..."
pkill -f "${APP_NAME}" 2>/dev/null || true
echo "Process stopped successfully."
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
两层保障:精确停止 + 模糊扫杀,确保无论什么异常情况,进程都能被终止。这是运维脚本的常见模式——先礼后兵,优先用 PID 精确定位,再用进程名兜底扫尾。
方法1(精确): PID文件 → kill 指定进程
│
│ 如果 PID 文件不存在 / 进程未被杀死
▼
方法2(兜底): pkill 按进程名扫杀所有匹配进程
2
3
4
5
# 3.2 脚本核心分析
方法1:PID 文件精确停止(主方案)
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
2
3
4
5
- 读取 PID 文件获取进程号 →
kill发送SIGTERM(默认信号) - 进程收到 SIGTERM → 进入
HandleSignal回调 → 优雅退出 2>/dev/null || true:进程已死时 kill 会报错,静默忽略- 最后删除 PID 文件,清理现场
方法2:进程名兜底(备用方案)
pkill -f "${APP_NAME}" 2>/dev/null || true
pkill -f按命令行匹配所有含iotservice的进程- 覆盖两种异常情况:
- PID 文件丢失或损坏(进程在但文件没了)
- 有残留子进程(PID 文件只记录主进程)