编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • README
    • QML基础入门

    • QT核心库实践

    • Linux实践开发

      • README
      • Linux开发指引
      • 崩溃监听实践
      • Linux应用指令
      • 应用重启策略
      • 守护进程保活
      • 脚本进程保活
      • 应用启动的原理
        • 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 脚本核心分析
      • LVGL设计原理
      • Qt应用保活方案设计
  • Apps
  • Linux应用开发
  • Linux实践开发
杨充
2025-09-18
目录

应用启动的原理

# 目录介绍

  • 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 几号进程?崩溃后谁来重启?
1
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,行为一致               │
└──────────────────────────────────────────────┘
1
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 文件锁                            │
└─────────────────────────────────────────────────────────────┘
1
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."
1
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/
1
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()
1
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
1
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 事件循环
1
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 事件循环(阻塞,直到收到退出信号)
1
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 自动释放锁
1
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."
1
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 按进程名扫杀所有匹配进程
1
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
1
2
3
4
5
  • 读取 PID 文件获取进程号 → kill 发送 SIGTERM(默认信号)
  • 进程收到 SIGTERM → 进入 HandleSignal 回调 → 优雅退出
  • 2>/dev/null || true:进程已死时 kill 会报错,静默忽略
  • 最后删除 PID 文件,清理现场

方法2:进程名兜底(备用方案)

pkill -f "${APP_NAME}" 2>/dev/null || true
1
  • pkill -f 按命令行匹配所有含 iotservice 的进程
  • 覆盖两种异常情况:
    1. PID 文件丢失或损坏(进程在但文件没了)
    2. 有残留子进程(PID 文件只记录主进程)
上次更新: 2026/06/10, 11:13:41
脚本进程保活
LVGL设计原理

← 脚本进程保活 LVGL设计原理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式