各位老铁,早上好!今天咱们来聊聊Python里的“隐身侠”—— daemon 进程,也就是传说中的后台服务。这玩意儿,听起来高大上,其实没那么神秘。 咱们的目标是:让你不仅知道啥是 daemon 进程,还能自己动手撸一个出来,并且把它管得服服帖帖的。
一、啥是 Daemon 进程?为啥要用它?
想象一下,你开了一家餐厅,总不能老板亲自端盘子、洗碗吧? 得找些默默无闻的“后台服务员”——它们在厨房里忙活,处理各种任务,但顾客(用户)根本看不到他们。
Daemon 进程,就是 Linux/Unix 系统里的这种“后台服务员”。它有以下特点:
- 独立自主: 脱离终端控制,不会因为你关掉终端就挂掉。
- 默默奉献: 在后台运行,不和用户直接交互(除非你专门设计了交互接口)。
- 持续工作: 一般来说,除非遇到错误或者被手动停止,否则会一直运行下去。
为啥要用 daemon 进程?
- 长时间运行的任务: 比如监控服务器状态、定时备份数据、处理网络请求等等,这些任务需要一直运行,不能依赖用户的登录状态。
- 提高系统资源利用率: 可以让程序在后台运行,释放终端资源,让用户可以继续做其他事情。
- 构建稳定可靠的服务: 可以通过各种机制(例如守护进程管理器)来保证 daemon 进程的稳定运行。
二、Daemon 进程的“变身”过程:关键步骤
让一个普通的 Python 脚本变成 daemon 进程,需要经历一系列的“变身”步骤。 咱们一步一步来,保证你听得明白,看得清楚。
-
脱离终端:
os.setsid()
这一步是关键,相当于给进程“断奶”,让它不再依赖于创建它的终端。
os.setsid()
会创建一个新的会话(session),让进程成为会话的领导者(session leader),并且脱离原来的控制终端。import os def daemonize(): """将当前进程变为守护进程""" # 1. Fork 一次,让父进程退出,子进程变成孤儿进程 pid = os.fork() if pid > 0: # 父进程退出 exit(0) elif pid < 0: # Fork 失败 exit(1) # 2. 子进程成为新的会话的领导者,脱离终端 os.setsid() # 3. 再次 Fork,防止进程重新获得控制终端 pid = os.fork() if pid > 0: # 父进程退出 exit(0) elif pid < 0: # Fork 失败 exit(1) # 现在,我们是真正的守护进程了
为啥要
fork()
两次?第一次
fork()
是为了让父进程退出,这样子进程就变成了孤儿进程,会被 init 进程收养。第二次
fork()
是为了防止进程重新获得控制终端。 如果进程是会话的领导者,并且没有控制终端,那么它可以打开一个终端设备来获取控制终端。 第二次fork()
之后,子进程不再是会话的领导者,就无法重新获得控制终端了。 -
改变工作目录:
os.chdir()
守护进程一般不应该依赖于当前的工作目录,所以最好把它改变到一个安全的地方,比如根目录
/
或者一个专门为它创建的目录。import os def daemonize(): # ... (前面的代码) ... # 4. 改变工作目录 os.chdir("/")
-
重定向标准输入、输出、错误:
sys.stdin
,sys.stdout
,sys.stderr
守护进程不需要和用户交互,所以可以把标准输入、输出、错误重定向到
/dev/null
,或者一个日志文件。import os import sys def daemonize(): # ... (前面的代码) ... # 5. 重定向标准输入、输出、错误 sys.stdin.close() sys.stdout.close() sys.stderr.close() sys.stdin = open('/dev/null', 'r') sys.stdout = open('/dev/null', 'w') # 或者一个日志文件 sys.stderr = open('/dev/null', 'w') # 或者一个日志文件
注意: 如果你需要记录守护进程的日志,应该把标准输出和标准错误重定向到一个日志文件,而不是
/dev/null
。 -
设置文件权限掩码:
os.umask()
守护进程创建的文件,权限应该有一定的限制。 可以使用
os.umask()
来设置文件权限掩码。import os def daemonize(): # ... (前面的代码) ... # 6. 设置文件权限掩码 os.umask(0) # 允许所有权限
os.umask()
是啥?os.umask()
设置的是一个“掩码”,它会从默认的文件权限中移除一些权限。 比如,如果umask
是0022
,那么新创建的文件权限会是644
(0666 – 0022 = 0644),目录权限会是755
(0777 – 0022 = 0755)。设置
os.umask(0)
意味着允许所有权限。 -
编写守护进程的主逻辑
import time import os import sys def daemonize(): # ... (前面的代码) ... pass def main(): """守护进程的主逻辑""" while True: try: with open("/tmp/daemon.log", "a") as f: f.write(f"{time.ctime()} - Daemon process is running...n") time.sleep(5) except Exception as e: print(f"Error: {e}") time.sleep(5) if __name__ == "__main__": daemonize() main()
这个
main()
函数就是守护进程要执行的任务。 这里只是简单地每 5 秒钟向/tmp/daemon.log
文件写入一条日志。
三、一个完整的 Daemon 进程示例
import os
import sys
import time
import atexit
import signal
class Daemon:
"""
一个通用的守护进程类。
用法: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile,
stdin='/dev/null',
stdout='/dev/null',
stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def daemonize(self):
"""
将当前进程变为守护进程。
"""
# 1. Fork 一次
try:
pid = os.fork()
if pid > 0:
# 父进程退出
sys.exit(0)
except OSError as e:
sys.stderr.write("fork #1 failed: %d (%s)n" % (e.errno, e.strerror))
sys.exit(1)
# 2. 脱离终端
os.setsid()
# 3. 再次 Fork
try:
pid = os.fork()
if pid > 0:
# 父进程退出
sys.exit(0)
except OSError as e:
sys.stderr.write("fork #2 failed: %d (%s)n" % (e.errno, e.strerror))
sys.exit(1)
# 4. 改变工作目录
os.chdir("/")
# 5. 设置文件权限掩码
os.umask(0)
# 6. 重定向标准输入、输出、错误
sys.stdout.flush()
sys.stderr.flush()
si = open(self.stdin, 'r')
so = open(self.stdout, 'a+')
se = open(self.stderr, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# 7. 注册退出函数,当进程退出时删除pidfile
atexit.register(self.delpid)
# 8. 将PID写入pidfile
pid = str(os.getpid())
open(self.pidfile,'w+').write("%sn" % pid)
def delpid(self):
os.remove(self.pidfile)
def start(self):
"""
启动守护进程
"""
# 1. 检查pidfile是否存在以判断进程是否已经运行
try:
pf = open(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid:
message = "pidfile %s already exist. Daemon already running?n"
sys.stderr.write(message % self.pidfile)
sys.exit(1)
# 2. 启动守护进程
self.daemonize()
self.run()
def stop(self):
"""
停止守护进程
"""
# 1. 从pidfile中获取PID
try:
pf = open(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if not pid:
message = "pidfile %s does not exist. Daemon not running?n"
sys.stderr.write(message % self.pidfile)
return # not an error in a restart
# 2. 尝试杀死守护进程
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
err = str(err)
if err.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err))
sys.exit(1)
def restart(self):
"""
重启守护进程
"""
self.stop()
self.start()
def run(self):
"""
你需要在子类中重写这个方法,
它将在守护进程启动后被调用。
"""
pass
# 示例用法
class MyDaemon(Daemon):
def run(self):
while True:
try:
with open("/tmp/mydaemon.log", "a") as f:
f.write(f"{time.ctime()} - MyDaemon is running...n")
time.sleep(5)
except Exception as e:
print(f"Error: {e}")
time.sleep(5)
if __name__ == "__main__":
daemon = MyDaemon('/tmp/mydaemon.pid',
stdout='/tmp/mydaemon.log',
stderr='/tmp/mydaemon.log')
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
else:
print ("Unknown command")
sys.exit(2)
sys.exit(0)
else:
print ("usage: %s start|stop|restart" % sys.argv[0])
sys.exit(2)
如何使用这个例子:
- 保存代码: 将代码保存为
mydaemon.py
。 - 启动守护进程: 运行
python mydaemon.py start
。 - 停止守护进程: 运行
python mydaemon.py stop
。 - 重启守护进程: 运行
python mydaemon.py restart
。 - 查看日志: 查看
/tmp/mydaemon.log
文件,可以看到守护进程的日志输出。 - 检查 PID 文件:
/tmp/mydaemon.pid
中保存了守护进程的 PID。
四、守护进程的管理:让它听话
光有 daemon 进程还不够,还得能管理它,让它按照我们的意愿启动、停止、重启。
-
PID 文件:
守护进程通常会把自己的 PID 写入一个文件(PID 文件),方便我们找到它,并向它发送信号。
在上面的代码中,我们使用了
/tmp/mydaemon.pid
作为 PID 文件。 -
信号处理:
可以使用信号来控制守护进程。常用的信号有:
信号 含义 SIGTERM
正常终止信号,告诉进程优雅地退出。 SIGKILL
强制终止信号,立即杀死进程(不建议使用)。 SIGHUP
挂起信号,通常用于重新加载配置文件。 在上面的代码中,
stop()
函数使用SIGTERM
信号来停止守护进程。 -
守护进程管理器:
手动管理守护进程太麻烦,可以使用守护进程管理器来自动管理。 常用的守护进程管理器有:
- Systemd: 现代 Linux 系统推荐使用的守护进程管理器。
- Supervisor: Python 编写的守护进程管理器,配置简单,功能强大。
- Upstart: Ubuntu 系统早期使用的守护进程管理器(现在已经被 Systemd 取代)。
使用 Systemd 管理守护进程:
-
创建 Systemd 服务文件:
在
/etc/systemd/system/
目录下创建一个.service
文件,比如mydaemon.service
。[Unit] Description=My Daemon Service After=network.target [Service] User=root # 运行用户 WorkingDirectory=/opt/mydaemon # 工作目录 ExecStart=/usr/bin/python3 /opt/mydaemon/mydaemon.py start # 启动命令 ExecStop=/usr/bin/python3 /opt/mydaemon/mydaemon.py stop # 停止命令 Restart=on-failure # 失败时自动重启 Type=forking # 进程类型 [Install] WantedBy=multi-user.target
-
启用服务:
运行以下命令来启用服务:
sudo systemctl enable mydaemon.service
-
启动服务:
运行以下命令来启动服务:
sudo systemctl start mydaemon.service
-
停止服务:
运行以下命令来停止服务:
sudo systemctl stop mydaemon.service
-
重启服务:
运行以下命令来重启服务:
sudo systemctl restart mydaemon.service
-
查看服务状态:
运行以下命令来查看服务状态:
sudo systemctl status mydaemon.service
五、Daemon 进程的注意事项
- 异常处理: 守护进程需要在
run()
方法中做好异常处理,防止程序崩溃。 - 日志记录: 守护进程需要记录详细的日志,方便排查问题。
- 资源释放: 守护进程需要及时释放资源,防止内存泄漏。
- 安全性: 守护进程应该以低权限用户运行,防止被恶意利用。
六、总结
Daemon 进程是 Python 开发中非常有用的技术,可以用来构建各种后台服务。 掌握 Daemon 进程的原理和使用方法,可以让你更好地管理你的 Python 程序,提高系统的稳定性和可靠性。
记住,代码是最好的老师。 多练习,多实践,你也能成为 Daemon 进程的高手!
好了,今天的分享就到这里, 祝大家编程愉快!