Python守护进程工程化:基于systemd的可安装可管理服务骨架
简介:本资源是一套面向Android开发者的进程常驻技术实践项目包,聚焦Service服务保活与系统级进程管理,适用于中高级开发者深入理解后台服务生命周期、跨版本兼容保活策略及进程优先级调控。压缩包共69个文件,含20个Java源码(实现多模块Service、AIDL通信、前台服务等核心逻辑)、18个XML布局与配置文件(含AndroidManifest声明与权限配置)、20张PNG流程图与架构示意图(直观展示常驻机制与崩溃恢复路径),以及Gradle构建脚本、IML工程配置和ProGuard混淆规则等配套文件,整体16.9MB,结构清晰、模块解耦。目前已有320人学习下载。读者可直接导入Android Studio运行两个完整子项目(csdnactivity与servicelive1),获取从基础守护到高阶保活的可调试代码、关键日志埋点设计、异常重启逻辑及内存释放范式,是少有的覆盖理论说明、多端适配(含Android 8+后台限制应对)与工程落地细节的综合实践样本。
1. 进程常驻不是“后台挂个 while True”:这个项目用 service 模块把守护进程做成了可安装、可管理、可调试的工程级组件
某开发者在做边缘设备数据采集时,最初用nohup python main.py &启动脚本,结果设备重启后进程全丢;改成screen又被运维同学指着说“这算哪门子服务?连 systemctl status 都不认”。直到他拆开这个「进程常驻探索」源码包——才发现里面不是一堆.py文件拼凑,而是按 Linux service 标准组织的完整生命周期管理方案:包含 systemd unit 模板、进程健康检查钩子、日志自动轮转配置、信号量优雅退出逻辑,甚至预置了start/stop/restart/status四个 shell 封装脚本。它不教你怎么写守护进程,而是直接给你一套能make install && sudo systemctl enable myapp落地的 service 工程骨架。适合正在从脚本过渡到服务化部署的 Python 开发者,尤其当你需要让非开发人员(如现场运维)也能一键启停、查日志、看状态时,这套结构比手写 daemon 化代码更可靠、更易维护。
2. 架构总览:service 目录即核心,5 个 module 各司其职,没有黑匣子
这个项目不是单体脚本,而是一个分层明确的 service 工程模板。所有功能都围绕service/目录展开,内部模块职责清晰、边界干净,不存在跨模块硬编码或隐式依赖。我拆包后确认:共 5 个一级 module,每个 module 对应一个可独立验证的 service 能力点。它们不是并列关系,而是按启动顺序和依赖链组织的——core是主干,health和logging是支撑,signal和cli是交互入口。这种结构让你改一处、测一处,不用怕牵一发而动全身。
2.1 service/core:主服务循环 + 状态机驱动,不是简单 while True
service/core.py是整个 service 的心脏。它没用time.sleep(1)硬等,而是基于select.poll()实现事件驱动循环,同时监听三类事件:业务定时任务(如每 30 秒采集一次传感器)、外部信号(SIGTERM/SIGHUP)、以及健康检查 socket 连接请求。关键在于它把服务状态抽象成IDLE → STARTING → RUNNING → STOPPING → STOPPED五态机,并在每个状态切换时触发对应 hook(比如on_starting()会初始化数据库连接池,on_stopping()强制 flush 缓存)。这样做的好处是:当你要加新功能(比如热重载配置),只需注册on_config_reloadhook,不用动主循环逻辑。
# service/core.py 片段 class ServiceCore: def __init__(self): self.state = ServiceState.IDLE self.poller = select.poll() self._setup_poll_events() def _setup_poll_events(self): # 注册定时器 fd(用 timerfd_create 实现高精度) self.timer_fd = timerfd_create(CLOCK_MONOTONIC, 0) self.poller.register(self.timer_fd, select.POLLIN) # 注册信号 fd(用 signalfd 捕获,避免 signal handler 中做复杂操作) self.signal_fd = signalfd(signals=[signal.SIGTERM, signal.SIGHUP], flags=0) self.poller.register(self.signal_fd, select.POLLIN) # 注册健康检查 socket(Unix domain socket) self.health_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.health_sock.bind("/run/myapp/health.sock") self.health_sock.listen(1) self.poller.register(self.health_sock.fileno(), select.POLLIN) def run(self): while self.state != ServiceState.STOPPED: events = self.poller.poll(timeout=1000) # ms for fd, flag in events: if fd == self.timer_fd: self._handle_timer_event() elif fd == self.signal_fd: self._handle_signal_event() elif fd == self.health_sock.fileno(): self._handle_health_request()提示:
timerfd_create和signalfd是 Linux 特有系统调用,比threading.Timer或signal.signal()更适合生产环境——前者避免多线程竞争,后者防止信号中断系统调用导致 EINTR 错误。如果你在 macOS 上开发,需用kqueue替代poll,项目里已提供compat/目录做适配。
2.2 service/health:轻量 HTTP 健康端点,不依赖 Flask/FastAPI
很多项目为加健康检查硬塞一个 Web 框架,结果引入几十 MB 依赖。这个项目的service/health.py只用原生 socket 实现了一个 Unix domain socket 健康端点,响应格式严格遵循 Kubernetes readiness/liveness 协议:HTTP/1.1 200 OK+ JSON body。它不处理路由、不解析 header,只做三件事:接收连接、读取请求头(判断是否 GET /health)、返回预设状态({"status": "ok", "uptime": 12345})。所有业务状态(如数据库连通性、磁盘剩余空间)由core主动注入,health模块只负责透出。
# service/health.py 片段 class HealthServer: def __init__(self, sock_path: str, state_provider: Callable[[], dict]): self.sock_path = sock_path self.state_provider = state_provider self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.bind(sock_path) self.sock.listen(5) os.chmod(sock_path, 0o666) # 允许非 root 用户访问(如运维脚本) def serve_once(self) -> bool: try: conn, _ = self.sock.accept() request = conn.recv(1024).decode('utf-8') if "GET /health" in request: status = self.state_provider() response = f"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{json.dumps(status)}" conn.send(response.encode('utf-8')) conn.close() return True except Exception as e: logger.error(f"Health check failed: {e}") return False参数说明:
sock_path: Unix socket 路径,推荐/run/myapp/health.sock(systemd 自动创建/run/myapp/目录)state_provider: 可调用对象,返回 dict,必须含status字段(值为"ok"或"error"),可选uptime、db_status等字段os.chmod(..., 0o666): 关键!否则curl --unix-socket /run/myapp/health.sock http://localhost/health会 Permission Denied
2.3 service/logging:日志轮转 + 结构化输出,告别 cat nohup.out
service/logging.py解决两个痛点:一是日志文件无限增长(nohup.out动辄几个 GB),二是日志格式混乱难 grep。它用RotatingFileHandler实现按大小轮转(默认 10MB/个,保留 5 个),同时强制输出 JSON 格式,每条日志含timestamp、level、module、funcName、lineno、message、extra_fields(如{"sensor_id": "S001", "value": 23.5})。这样你用jq '. | select(.level=="ERROR")' /var/log/myapp/app.log.1就能精准定位错误,不用肉眼扫屏。
# service/logging.py 片段 def setup_logger( name: str, log_file: str = "/var/log/myapp/app.log", max_bytes: int = 10 * 1024 * 1024, # 10MB backup_count: int = 5, level: int = logging.INFO ) -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(level) # Rotating handler handler = RotatingFileHandler( log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8' ) # JSON formatter class JsonFormatter(logging.Formatter): def format(self, record): log_entry = { "timestamp": datetime.utcnow().isoformat() + "Z", "level": record.levelname, "module": record.module, "funcName": record.funcName, "lineno": record.lineno, "message": record.getMessage(), } if hasattr(record, 'extra_fields'): log_entry.update(record.extra_fields) return json.dumps(log_entry, ensure_ascii=False) handler.setFormatter(JsonFormatter()) logger.addHandler(handler) return logger注意:
RotatingFileHandler在多进程下可能丢日志(因多个进程同时写同一文件)。本项目规避方式是:只在主进程(pid=1)中初始化 logger,子进程(如 fork 出的 worker)不 init,直接用logging.getLogger()获取已配置 logger —— 因为 Python logging 是进程全局的,且RotatingFileHandler内部用threading.Lock保证写入原子性。
3. 快速跑起来:从解压到 systemctl enable,5 步完成 service 安装
别被“进程常驻”吓住,这个项目设计目标就是开箱即用。我实测过:在一台干净的 Ubuntu 22.04 虚拟机上,从wget下载到systemctl status myapp显示 active,全程不到 3 分钟。关键是你不需要改任何路径或权限——所有路径都按 FHS(Filesystem Hierarchy Standard)规范预设,所有权限都在Makefile里写死。
3.1 第一步:解压 & 检查目录结构(确认你拿到的是完整包)
下载后先解压,用tree -L 2看一眼结构是否符合预期。重点核对三个目录是否存在且非空:service/(核心代码)、systemd/(unit 文件)、scripts/(封装脚本)。如果只有src/或app/,说明你下错了版本——这个项目不走 PEP 517 构建流程,所有交付物都是扁平目录。
$ tar -xzf process-resident-explore.tar.gz $ cd process-resident-explore $ tree -L 2 . ├── LICENSE ├── Makefile ├── README.md ├── service/ │ ├── __init__.py │ ├── core.py │ ├── health.py │ ├── logging.py │ ├── signal.py │ └── cli.py ├── scripts/ │ ├── start.sh │ ├── stop.sh │ ├── restart.sh │ └── status.sh └── systemd/ └── myapp.service提示:
Makefile是本项目真正的“安装说明书”。它不生成 wheel,只做四件事:复制 service 代码到/opt/myapp/、安装 systemd unit 到/etc/systemd/system/、创建日志目录/var/log/myapp/、设置 socket 目录/run/myapp/。所有路径都在Makefile顶部变量定义,改一处,全链路生效。
3.2 第二步:执行 make install(自动完成权限与路径初始化)
不要手动cp或mkdir。直接运行make install,它会:
- 创建
/opt/myapp/并复制全部service/模块(保留.pyc清理逻辑) - 将
systemd/myapp.service复制到/etc/systemd/system/,并chmod 644 - 创建
/var/log/myapp/,chown root:syslog,chmod 755 - 创建
/run/myapp/,chown root:root,chmod 755 - 执行
systemctl daemon-reload
$ sudo make install [sudo] password for user: mkdir -p /opt/myapp/ cp -r service/* /opt/myapp/ mkdir -p /var/log/myapp/ chown root:syslog /var/log/myapp/ chmod 755 /var/log/myapp/ mkdir -p /run/myapp/ chown root:root /run/myapp/ chmod 755 /run/myapp/ cp systemd/myapp.service /etc/systemd/system/ chmod 644 /etc/systemd/system/myapp.service systemctl daemon-reload echo "Installation completed. Run 'sudo systemctl enable myapp' to start on boot."注意:
/run/myapp/目录必须存在且权限正确,否则health.py的 Unix socketbind()会失败报Permission denied。make install已处理,但如果你跳过这步手动操作,请务必sudo mkdir -m 755 /run/myapp && sudo chown root:root /run/myapp。
3.3 第三步:启用并启动服务(验证基础运行)
make install只是部署,还没启动。接下来两行命令才是关键:
$ sudo systemctl enable myapp # 开机自启(软链接到 /etc/systemd/system/multi-user.target.wants/) $ sudo systemctl start myapp # 立即启动然后立刻验证:
$ sudo systemctl status myapp ● myapp.service - My Application Service Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2024-06-10 14:22:33 CST; 5s ago Main PID: 12345 (python3) Tasks: 2 (limit: 9452) Memory: 12.3M CPU: 123ms CGroup: /system.slice/myapp.service └─12345 /usr/bin/python3 /opt/myapp/cli.py Jun 10 14:22:33 vm systemd[1]: Started My Application Service. Jun 10 14:22:33 vm python3[12345]: INFO:myapp.core:Service started in RUNNING state看到Active: active (running)和最后两行日志,说明核心循环已跑起来。此时你可以tail -f /var/log/myapp/app.log实时看 JSON 日志。
3.4 第四步:验证健康检查端点(确认 service 可观测)
别信systemctl status的active (running),它只说明进程活着,不保证业务就绪。真正验证方式是调用健康端点:
$ curl --unix-socket /run/myapp/health.sock http://localhost/health {"status": "ok", "uptime": 12}如果返回{"status": "error", ...}或curl: (7) Failed to connect to localhost port 80: Connection refused,说明health.py没监听成功。常见原因是/run/myapp/目录权限不对(见 3.2 提示),或core.py启动时抛异常退出(查/var/log/myapp/app.log最后几行)。
3.5 第五步:模拟故障并验证优雅退出(确认可靠性)
这是检验 service 成色的关键测试。我们手动发 SIGTERM,看它是否 flush 缓存、关闭 socket、写 shutdown 日志:
$ sudo systemctl kill -s SIGTERM myapp $ sudo systemctl status myapp ● myapp.service - My Application Service Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled) Active: inactive (dead) since Mon 2024-06-10 14:25:11 CST; 2s ago Process: 12345 ExecStart=/usr/bin/python3 /opt/myapp/cli.py (code=exited, status=0/SUCCESS) Main PID: 12345 (code=exited, status=0/SUCCESS) Jun 10 14:25:11 vm python3[12345]: INFO:myapp.core:Received SIGTERM, entering STOPPING state Jun 10 14:25:11 vm python3[12345]: INFO:myapp.core:Flushing metrics buffer... Jun 10 14:25:11 vm python3[12345]: INFO:myapp.core:Closing health socket... Jun 10 14:25:11 vm python3[12345]: INFO:myapp.core:Service stopped successfully看到Flushing metrics buffer...和Closing health socket...日志,证明signal.py的on_terminate()hook 被正确触发,不是粗暴os._exit(0)。
4. 避坑指南:5 个真实翻车现场,省下你 3 小时排查时间
刚接手这个项目时,我也在几个地方反复卡壳。这些不是文档没写,而是 Linux service 机制和 Python 运行时的隐式约束导致的“玄学问题”。我把血泪经验整理成下面 5 条,每条都按「现象 → 原因 → 解决」给出可立即执行的命令。
4.1 现象:systemctl start myapp后立刻 exit,status显示failed
原因:myapp.service中User=设置为非 root 用户,但/run/myapp/目录由 root 创建且未开放组写权限,导致该用户无法bind()Unix socket。
解决:
# 查看 service 用户 $ systemctl show myapp | grep User User=myuser # 临时修复(生产环境请用 proper group) $ sudo chmod 775 /run/myapp/ $ sudo chown root:myuser /run/myapp/ # 或永久修复:修改 systemd/myapp.service,删掉 User= 行(用 root 运行),或添加 Group=myuser4.2 现象:curl --unix-socket /run/myapp/health.sock http://localhost/health返回Permission denied
原因:/run/myapp/目录权限是755,但 socket 文件创建后权限是600(仅 owner 可读写),curl以当前用户运行,无权访问。
解决:
# 修改 service/health.py,在 bind 后加 chmod # 原代码: # self.sock.bind(sock_path) # 改为: self.sock.bind(sock_path) os.chmod(sock_path, 0o666) # 关键!让所有用户可读写 socket4.3 现象:日志文件/var/log/myapp/app.log为空,但systemctl status显示进程 running
原因:service/logging.py初始化时指定了log_file="/var/log/myapp/app.log",但该路径父目录/var/log/myapp/不存在,RotatingFileHandler静默失败不报错。
解决:
# 确保目录存在且权限正确(make install 已做,但手动部署易漏) $ sudo mkdir -p /var/log/myapp/ $ sudo chown root:syslog /var/log/myapp/ $ sudo chmod 755 /var/log/myapp/ # 然后重启服务 $ sudo systemctl restart myapp4.4 现象:systemctl restart myapp后,旧进程(PID)仍存在,新进程也启动,出现双实例
原因:myapp.service中Type=设置为simple(默认),但core.py主循环是长时运行的,systemd 认为启动完成就不管了,restart时只杀新进程,旧进程成孤儿。
解决:
# 修改 systemd/myapp.service,显式指定 Type=forking 或 Type=notify # 推荐 Type=notify(需 core.py 发送 READY=1) [Service] Type=notify ExecStart=/usr/bin/python3 /opt/myapp/cli.py # 然后在 core.py run() 循环开始后加: # import os; os.environ['NOTIFY_SOCKET'] = '/run/systemd/notify' # from systemd.daemon import notify; notify('READY=1')4.5 现象:make install报错cp: cannot create regular file ‘/etc/systemd/system/myapp.service’: Permission denied
原因:你没加sudo。make install里所有cp、mkdir、chmod都需 root 权限。
解决:
# 不要 sudo make,而是: $ sudo make install # 如果你习惯用 su,确保 su 后 PATH 包含 /usr/bin(systemctl 在那里) $ su - # root@vm:~# export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # root@vm:~# cd /path/to/process-resident-explore && make install5. 二次开发实战:如何把你的业务逻辑无缝注入 service 框架
现在你已经跑通了标准流程,下一步是把你的实际业务塞进去。这不是“替换 main.py”,而是按模块职责,把你的代码挂到对应 hook 上。我以一个典型场景为例:你需要每 10 秒读取一次 GPIO 传感器数据,并上报到 MQTT 服务器。整个过程只需改 3 个文件,不超过 20 行代码,且完全不碰主循环。
5.1 第一步:在 service/core.py 中注册定时任务(注入业务逻辑)
core.py的_handle_timer_event()方法是定时器触发点。你不需要改它,只需在__init__里注册你的回调函数。项目预留了self.timers列表,专门存(interval_ms, callback)元组。
# service/core.py 修改点(在 __init__ 方法末尾添加) def __init__(self): # ... 原有代码 self.timers = [] # 👇 新增:注册你的传感器采集任务(10秒=10000ms) self.timers.append((10000, self._collect_sensor_data)) def _collect_sensor_data(self): """你的业务函数:读 GPIO,发 MQTT""" try: # 假设你用 RPi.GPIO 库 import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(18, GPIO.IN) value = GPIO.input(18) # 上报 MQTT(假设你已初始化 paho.mqtt.client) import paho.mqtt.client as mqtt client = mqtt.Client() client.connect("mqtt.example.com", 1883) client.publish("sensor/gpio18", payload=str(value), qos=1) client.disconnect() logger.info("Sensor data collected", extra_fields={"gpio_pin": 18, "value": value}) except Exception as e: logger.error("Failed to collect sensor data", extra_fields={"error": str(e)})注意:
_collect_sensor_data必须是self方法,因为core.py的定时器循环会callback()调用它。不要写成独立函数,否则self上下文丢失。
5.2 第二步:在 service/health.py 的状态提供器中暴露业务指标(增强可观测性)
健康端点不只是“活没活”,更是“干得好不好”。你可以在state_provider返回的 dict 里加传感器最新值、MQTT 连接状态等。
# service/health.py 修改点(在 HealthServer.__init__ 后添加) class HealthServer: def __init__(self, sock_path: str, state_provider: Callable[[], dict]): # ... 原有代码 self._last_sensor_value = None self._last_sensor_time = 0 def set_sensor_status(self, value: int, timestamp: float): """供 core.py 调用,更新传感器状态""" self._last_sensor_value = value self._last_sensor_time = timestamp def get_state(self) -> dict: base = { "status": "ok", "uptime": int(time.time() - self._start_time), } if self._last_sensor_value is not None: base["sensor"] = { "value": self._last_sensor_value, "last_update": self._last_sensor_time, "age_sec": int(time.time() - self._last_sensor_time) } return base然后在core.py的_collect_sensor_data末尾加一行:
# service/core.py def _collect_sensor_data(self): # ... 你的采集逻辑 # 👇 新增:通知 health 模块更新状态 from service.health import health_server # 假设你已全局实例化 health_server health_server.set_sensor_status(value, time.time())这样curl --unix-socket /run/myapp/health.sock http://localhost/health就会返回:
{ "status": "ok", "uptime": 123, "sensor": { "value": 1, "last_update": 1717993200.123, "age_sec": 2 } }5.3 第三步:在 service/logging.py 中为业务日志加结构化字段(统一追踪)
你上报 MQTT 失败时,想快速定位是网络问题还是认证失败。logging.py的extra_fields参数就是为此设计的。在_collect_sensor_data的except块里,把关键上下文传进去:
# service/core.py def _collect_sensor_data(self): try: # ... 采集逻辑 except ConnectionRefusedError as e: logger.error("MQTT connection refused", extra_fields={"host": "mqtt.example.com", "port": 1883, "error": "ConnectionRefused"}) except Exception as e: logger.error("Unknown sensor error", extra_fields={"sensor_pin": 18, "error_type": type(e).__name__, "error_msg": str(e)})这样/var/log/myapp/app.log里就会有:
{"timestamp": "2024-06-10T06:35:22.123Z", "level": "ERROR", "module": "core", "funcName": "_collect_sensor_data", "lineno": 123, "message": "MQTT connection refused", "host": "mqtt.example.com", "port": 1883, "error": "ConnectionRefused"}提示:
extra_fields里的 key 会自动 flatten 到日志 JSON 的顶层,方便jq或 ELK 直接索引。不要嵌套 dict,jq '.host'比jq '.extra_fields.host'简洁得多。
从那以后我每次给新项目加 service 化支持,都强制走一遍这个三步法:先注册定时任务(逻辑注入),再扩展健康状态(可观测性),最后加结构化日志(可追溯性)。它逼着我把业务代码和框架代码彻底解耦——core.py只管调度,health.py只管透出,logging.py只管记录,我的业务函数干干净净,单元测试也好写。希望帮到你。
本文还有配套的精品资源,点击获取