前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >2.并发编程~先导篇(下)

2.并发编程~先导篇(下)

作者头像
逸鹏
发布于 2018-08-14 06:28:57
发布于 2018-08-14 06:28:57
1.4K00
代码可运行
举报
文章被收录于专栏:逸鹏说道逸鹏说道
运行总次数:0
代码可运行

先写几个问号来概况下今天准备说的内容:(谜底自己解开,文中都有)

  1. 你知道 Ctrl+C终止进程的本质吗?你知道 Kill-9pid的真正含义吗?
  2. 你知道那些跨平台框架(Python,NetCore)在Linux下创建进程干了啥?
  3. 你了解 僵尸进程孤儿进程的悲催生产史吗? 孤儿找干爹僵尸送往生想知道不?
  4. 想知道创建子进程后怎么 李代桃僵吗? ps aux|grep xxx的背后到底隐藏了什么?
  5. 你了解Linux磁盘中p类型的文件到底是个啥吗?
  6. 为什么要realase发布而不用debug直接部署?这背后的性能相差几何?
  7. 还有更多进程间的 密密私语等着你来查看哦~

上回说道:1.并发编程~先导篇(上)

2.4.5.进程间通信~MMAP内存映射(常用)

代码实例:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/进程通信/5.mmap

好处:内存操作,比IO快

缺点:和文件一样不会像管道一样阻塞(读的可能不全,需要自己考虑读写效率)

画个简单的图示:

PS:内存映射一个文件并不会导致整个文件被读取到内存中:

  1. 文件并没有被复制到内存缓存中,操作系统仅仅为文件内容保留了一段虚拟内存。
  2. 当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。
  3. 没有访问的部分还是留在磁盘上

以Linux为例,简单解析一下帮助文档:(加粗的是必填参数)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mmap.mmap(fileno,length[,flags=MAP_SHARED][,prot=PROT_WRITE|PROT_READ][,access=ACCESS_DEFAULT][,offset]
  1. fileno:就是我们经常说的 文件描述fd
    1. 可以通过 os.open()直接打开fd
    2. 也可以调用文件的 f.fileno()
  2. length:映射区大小,(一般写0就OK了)
    1. 如果length为0,则映射的最大长度将是调用时文件的当前大小
    2. 一般把 文件大小os.path.getsize(path)传进去就可以了
  3. flags:映射区性质,默认是用共享
    1. MAPSHARED 共享(数据会自动同步磁盘)
    2. MAPPRIVATE 私有(不同步磁盘)
  4. prot:映射区权限,如果指定,就会提供内存保护(默认即可)
    1. PROT_READ 读
    2. PROTREAD | PROTWRITE 写(必须有读的权限)
  5. access:可以指定访问来代替flags和prot作为可选的关键字参数【这个是Python为了简化而添加的】
    1. ACCESSREAD:只读
    2. ACCESSWRITE:读和写(会影响内存和文件)
    3. ACCESSCOPY:写时复制内存(影响内存,但不会更新基础文件)
    4. ACCESSDEFAULT:延迟到prot(3.7才添加)
  6. offset,偏移量,默认是0(和文件一致)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 这个够明了了,\0转换成二进制就是\x00
"\0".encode()
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
b'\x00'
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 老规矩,开始之前,扩充一个小知识点:(空字符串和'\0'区别)
a = "" # 空字符串 (Python里面没有char类型)
b = "\x00" # `\0` 的二进制写法
c = "\0"

print(a)
print(b)
print(c)

print(len(a))
print(len(b))
print(len(c))
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0
1
1

看个简单的案例快速熟悉mmap模块:(大文件处理这块先不说,以后要是有机会讲数据分析的时候会再提)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
m.size()  # 查看文件大小
m.seek(0)  # 修改Postion位置
m.tell()  # 返回 m 对应文件的Postion位置
m.read().translate(None, b"\x00")  # 读取所有内容并把\0删除
m.closed  # 查看mmap是否关闭

# 支持切片操作
m[0:10] # 取值
m[0:10] = b"1234567890"  # 赋值

# 对自行模式大文件处理的同志,给个提示
m.readline().decode() # 读一行,并转换成str
m.size()==m.tell() # while循环退出条件

熟悉一下上面几个方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import mmap

def create_file(filename, size):
    """初始化一个文件,并把文件扩充到指定大小"""
    with open(filename, "wb") as f:
        f.seek(size - 1)  # 改变流的位置
        f.write(b"\x00")  # 在末尾写个`\0`

def main():
    create_file("mmap_file", 4096)  # 创建一个4k的文件
    with mmap.mmap(os.open("mmap_file", os.O_RDWR), 0) as m:  # 创建映射
        print(m.size())  # 查看文件大小
        m.resize(1024)  # 重新设置文件大小
        print(len(m))  # len也一样查看文件大小
        print(m.read().translate(None, b"\x00"))  # 读取所有内容并把\0删除
        print(m.readline().decode())  # 读取一行,bytes转成str
        print(m.tell())  # 返回 m 对应文件的当前位置
        m.seek(0)  # 修改Postion位置
        print(m.tell())  # 返回 m 对应文件的当前位置
        print(m[0:10])  # 支持切片操作
        print("postion_index:%d" % m.tell())
        m[0:10] = b"1234567890"  # 赋值
        print("postion_index:%d" % m.tell())
        print(m[0:10])  # 取值
        print("postion_index:%d" % m.tell())
        print(m[:].decode())  # 全部读出来
    print(m.closed)  # 查看mmap是否关闭

if __name__ == '__main__':
    main()

输出:(测试了一下,切片操作【读、写】不会影响postion)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
4096
1024
b''

1024
0
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
postion_index:0
postion_index:0
b'1234567890'
postion_index:0
1234567890
True

看下open打开的案例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import mmap

def main():
    with open("temp", "wb") as f:
        f.write("小明同学最爱刷碗\n小潘同学最爱打扫".encode())

    # 打开磁盘二进制文件进行更新(读写)
    with open("temp", "r+b") as f:
        with mmap.mmap(f.fileno(), 0) as m:
            print("postion_index:%d" % m.tell())
            print(m.readline().decode().strip())  # 转成str并去除两端空格
            print("postion_index:%d" % m.tell())
            print(m[:].decode())  # 全部读出来
            print("postion_index:%d" % m.tell())
            m.seek(0)
            print("postion_index:%d" % m.tell())

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
postion_index:0
小明同学最爱刷碗
postion_index:25
小明同学最爱刷碗
小潘同学最爱打扫
postion_index:25
postion_index:0

其他方法可以参考:这篇文章(Python3很多都和Python2不太相同,辩证去看吧)

注意一点:

通过MMap内存映射之后,进程间通信并不是对文件操作,而是在内存中。文件保持同步只是因为mmap的flags默认设置的是共享模式(MAP_SHARED)

PS:还记得之前讲类方法和实例方法的时候吗?Python中类方法可以直接被对象便捷调用,这边mmap实例对象中的方法,其实很多都是类方法步入正轨

来看一个有血缘关系的通信案例:(一般用匿名)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import mmap

def create_file(file_name, size):
    with open(file_name, "wb") as f:
        f.seek(size - 1)
        f.write(b"\0x00")

def main():
    file_name = "temp.bin"
    # mmap映射的时候不能映射空文件,所以我们自己创建一个
    create_file(file_name, 1024)

    fd = os.open(file_name, os.O_RDWR)
    with mmap.mmap(fd, 0) as m:  # m.resize(1024) # 大小可以自己调整的
        pid = os.fork()
        if pid == 0:
            print("[子进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
            m.write("子进程说:老爸,我想出去玩了~\n".encode())
            time.sleep(3)
            print(m.readline().decode().strip())
            exit(0)
        elif pid > 0:
            print("[父进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
            time.sleep(1)  # 和文件一样,非堵塞
            print(m.readline().decode().strip())
            m.write("父进程说:去吧去吧\n".encode())
            wpid, status = os.wait()
            print("[父进程]收尸:PID:%d,Status:%d" % (wpid, status))
            exit(0)

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[父进程]PID:6843PPID:3274
[子进程]PID:6844PPID:6843
子进程说:老爸,我想出去玩了~
父进程说:去吧去吧
[父进程]收尸:PID:6844,Status:0
有血缘关系使用MMAP通信

父进程创建了一份mmap对象,fork产生子进程的时候相当于copy了一份指向,所以可以进行直接通信(联想fd的copy)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import mmap

def main():
    # 不记录文件中,直接内存中读写(这个地方len就不能为0了,自己指定一个大小eg:4k)
    with mmap.mmap(-1, 4096) as m:
        pid = os.fork()
        if pid == 0:
            print("[子进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
            m.write("[子进程]老爸我出去嗨了~\n".encode())
            time.sleep(2)
            msg = m.readline().decode().strip()
            print(msg)
            exit(0)
        elif pid > 0:
            print("[父进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
            time.sleep(1)
            msg = m.readline().decode().strip()
            print(msg)
            m.write("[父进程]去吧,皮卡丘~".encode())

            wpid, status = os.wait()
            print("[父进程]收尸:PID:%d,Status:%d" % (wpid, status))
            exit(0)

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[父进程]PID:8115PPID:3274
[子进程]PID:8116PPID:8115
[子进程]老爸我出去嗨了~
[父进程]去吧,皮卡丘~
[父进程]收尸:PID:8116,Status:0
无血缘关系使用MMAP通信

因为不同进程之前没有关联,必须以文件为媒介(文件描述符fd)

进程1:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import mmap

def create_file(file_name, size):
    with open(file_name, "wb") as f:
        f.seek(size - 1)
        f.write(b"\0x00")

def main():
    file_name = "temp.bin"

    if not os.path.exists(file_name):
        # mmap映射的时候不能映射空文件,所以我们自己创建一个
        create_file(file_name, 1024)

    fd = os.open(file_name, os.O_RDWR)
    with mmap.mmap(fd, 0) as m:  # m.resize(1024) # 大小可以自己调整的
        print("[进程1]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
        m.write("进程1说:小明放学去撸串吗?\n".encode())
        time.sleep(3)
        print(m.readline().decode().strip())
        exit(0)

if __name__ == '__main__':
    main()

进程2:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import mmap

def create_file(file_name, size):
    with open(file_name, "wb") as f:
        f.seek(size - 1)
        f.write(b"\0x00")

def main():
    file_name = "temp.bin"

    if not os.path.exists(file_name):
        # mmap映射的时候不能映射空文件,所以我们自己创建一个
        create_file(file_name, 1024)

    fd = os.open(file_name, os.O_RDWR)
    with mmap.mmap(fd, 0) as m:  # m.resize(1024) # 大小可以自己调整的
        print("[进程2]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
        time.sleep(1)
        print(m.readline().decode().strip())
        m.write("进程2说:为毛不去?\n".encode())
        exit(0)

if __name__ == '__main__':
    main()

输出图示:

2.4.6.进程间通信~Signal信号

代码实例:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/进程通信/6.signal

信号:它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

一般信号不太用于进程间通信,常用就是发个信号把xxx进程干死。

先来个例子,等会讲理论:

Python里面一般用 os.kill(pid,signalnum)来发信号:eg: kill9pid

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import signal

def main():
    pid = os.fork()
    if pid == 0:
        print("[子进程]PID=%d,PPID=%d" % (os.getpid(), os.getppid()))
        while True:
            print("[子进程]孩子老卵,怎么滴吧~")
            time.sleep(1)
    elif pid > 0:
        print("[父进程]PID=%d,PPID=%d" % (os.getpid(), os.getppid()))
        time.sleep(3)
        print("父进程耐心有限,准备杀了儿子")

        # sigkill 相当于kill 9 pid
        os.kill(pid, signal.SIGKILL)  # 发信号

        # 收尸
        wpid, status = os.wait()
        print("父进程收尸:子进程PID=%d,Status=%d" % (wpid, status))

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[父进程]PID=21841,PPID=5559
[子进程]PID=21842,PPID=21841
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
父进程耐心有限,准备杀了儿子
父进程收尸:子进程PID=21842,Status=9

扩展一下:

  1. signal.pthread_kill(thread_id,signal.SIGKILL))# 杀死线程
  2. os.abort()# 给自己发异常终止信号
理论开始

这边开始说说理论:

信号状态

  1. 产生状态
  2. 未决状态(信号产生后没有被处理)
  3. 递达状态(信号已经传达到进程中)

产生、传递等都是通过内核进行的,结合上面的例子画个图理解下:

未决信号集:没有被当前进程处理的信号集合(可以通过 signal.sigpending()获取 set集合)

阻塞信号集:要屏蔽的信号(不能被用户操作)

回顾一下上面说 kill9pid原理的知识: kill-l

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1) SIGHUP     2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT     7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV    12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN    22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

说下常用的几个信号:

  1. 9号信号( sigkill)是 kill9
  2. 2号信号( sigint)是 Ctrl+C终止进程
  3. 3号信号( sigquit)是 Ctrl+\终止进程
信号捕捉

现在说说信号捕捉 signal.signal(signalnum,handler)

handler处理函数,除了自定义信号处理函数外也可以使用系统提供的两种方式:

  1. SIG_IGN(忽略该信号)
  2. SIG_DFL(系统默认操作)

注意一点: SIGSTOPSIGKILL 信号是不能被捕获、忽略和阻塞的(这个是系统预留的,如果连预留都没有可以想象肯定木马横向)

PS:信号的优先级一般都是比较高的,往往进程收到信号后都会停下手上的事情先处理信号(死循环也一样歇菜)

来看一个例子:(处理singint,忽略sigquit)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import signal

def print_info(signalnum, frame):
    print("信号:%d准备弄我,我是小强我怕谁?(%s)" % (signalnum, frame))

def main():
    signal.signal(signal.SIGINT, print_info)  # 处理Ctrl+C的终止命令(singint)
    signal.signal(signal.SIGQUIT, signal.SIG_IGN)  # 忽略Ctrl+\的终止命令(sigquit)

    while True:
        print("[PID:%d]我很坚强,不退出,等着信号来递达~" % os.getpid())
        time.sleep(3)  # 你要保证进程不会退出才能处理信号,不用担心影响信号(优先级高)

if __name__ == '__main__':
    main()

输出图示:(我休息3s,在3s内给程序发送了 sigint信号(Ctrl+C)就立马处理了)

扩展:

  1. 如果你只是等一个信号就退出,可以使用: signal.pause(),不必使用死循环来轮询了
  2. os.killpg(pgid,sid)进程组结束
  3. signal.siginterrupt(signal.SIGALRM,False) 防止系统调用被信号打断所设立(其实一般也不太用,出问题才用)

通俗的讲就是,要是系统和你发一样的信号可能也就被处理了,加上这句就ok了,eg:

举个例子,有时候有些恶意程序蓄意破坏或者被所谓的安全软件误杀比如系统函数 kill(-1)【有权限的都杀了】

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import signal

def print_info(signalnum, frame):
    print("死前留言:我被信号%d弄死了,记得替我报仇啊!" % signalnum)

def main():
    signal.signal(signal.SIGINT, print_info)  # 处理Ctrl+C的终止命令(singint)
    signal.signal(signal.SIGQUIT, print_info)  # 处理Ctrl+\的终止命令(singquit)
    signal.siginterrupt(signal.SIGINT, False)
    signal.siginterrupt(signal.SIGQUIT, False)
    signal.pause()  # 设置一个进程到休眠状态直到接收一个信号

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
dnt@MZY-PC:~/桌面/work/BaseCode/python/5.concurrent/Linux/进程通信/6.signal python3 1.os_kill2.py 
^C死前留言:我被信号2弄死了,记得替我报仇啊!
dnt@MZY-PC:~/桌面/work/BaseCode/python/5.concurrent/Linux/进程通信/6.signal python3 1.os_kill2.py 
^\死前留言:我被信号3弄死了,记得替我报仇啊!
dnt@MZY-PC:~/桌面/work/BaseCode/python/5.concurrent/Linux/进程通信/6.signal
定时器alarm(执行一次)

再说两个定时器就进下一个话题把,这个主要就是信号捕捉用得比较多,然后就是一般都是守护进程发信号

先验证一个概念:alarm闹钟不能被fork后的子进程继承

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import signal

def main():
    # 不受进程影响,每个进程只能有一个定时器,再设置只是重置
    signal.alarm(3)  # 设置终止时间(3s),然后终止进程(sigaltirm)

    pid = os.fork()
    if pid == 0:
        print("[子进程]PID=%d,PPID=%d" % (os.getpid(), os.getppid()))
        for i in range(5):
            print("[子进程]孩子老卵,怎么滴吧~")
            time.sleep(1)
    elif pid > 0:
        print("[父进程]PID=%d,PPID=%d" % (os.getpid(), os.getppid()))

    print("[遗言]PID=%d,PPID=%d" % (os.getpid(), os.getppid()))

if __name__ == '__main__':
    main()

输出

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[父进程]PID=9687,PPID=9063
[遗言]PID=9687,PPID=9063
[子进程]PID=9688,PPID=9687
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[遗言]PID=9688,PPID=1060

这个你可以自己验证:不受进程影响,每个进程只能有一个定时器,再设置只是重置

普及一个小技巧

其实好好看逆天的问题都会发现各种小技巧的,所有小技巧自我总结一下就会产生质变了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import signal

def main():
    signal.alarm(1)  # 设置终止时间(3s),然后终止进程(sigaltirm)
    i = 0
    while True:
        print(i)
        i += 1  # 别忘记,Python里面没有++哦~

if __name__ == '__main__':
    main()

运行一下: time python3 xxx.py

运行一下: time python3 xxx.py>temp

简单说下三个参数:

  1. real总共运行时间(real=user+sys+损耗时间)
  2. user(用户代码真正运行时间)
  3. sys(内核运行时间)【内核不运行,你系统也不正常了】

其实就是减少了IO操作,性能方面就相差几倍!我这边只是一台老电脑,要是真在服务器下性能相差可能让你吓一跳

现在知道为什么要realase发布而不用debug直接部署了吧(线上项目非必要情况,一般都会删除所有日记输出的

定时器setitimer(周期执行)

signal.setitimer(which,seconds,interval=0.0) which参数说明:

  1. signal.TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号
  2. signal.ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
  3. signal.ITIMERPROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMERVIRTUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

这个一般在守护进程中经常用,看个简单案例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import time
import signal

def say_hai(signalnum, frame):
    print("我会周期性执行哦~")

def main():
    # 捕捉信号(在前面最好,不然容易漏捕获)
    signal.signal(signal.SIGALRM, say_hai)
    # 设置定时器,第一次1s后执行,以后都3s执行一次
    signal.setitimer(signal.ITIMER_REAL, 1, 3)
    # print(signal.getitimer(signal.ITIMER_REAL))

    while True:
        print("我在做其他事情")
        time.sleep(1)

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
...

2.4.7.进程守护

实例代码:"https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/进程守护

守护进程应用场景很多,比如程序上线后有个bug被不定时的触发,每次都导致系统爆卡或者退出,而程序员修复bug需要时间,但是线上项目又不能挂,这时候就可以使用一个心跳检测的守护进程(查错也可以使用守护进程)【为恶就不说了】

正式开始前,先来个伪案例:

模拟一个漏洞百出的程序

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time

def main():
    print("[PID:%d]进程运行中..." % os.getpid())
    time.sleep(5)
    os.abort()  # 给自己发异常终止信号

if __name__ == '__main__':
    main()

写个简单版本的守护进程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import signal

def write_log(msg):
    pass

def is_running(p_name):
    """是否在运行"""
    try:
        # grep -v grep 不显示grep本身,wc -l是计数用的
        result = os.popen("ps ax | grep %s | grep -v grep" % p_name).readlines()
        if len(result) > 0:
            return True
        else:
            return False
    except Exception as ex:
        write_log(ex)
        return False

def is_restart(p_script):
    """重启程序"""
    try:
        if os.system(p_script) == 0:
            return True
        else:
            return False
    except Exception as ex:
        write_log(ex)
        return False

def heartbeat(signalnum, frame):
    """心跳检查"""
    p_name = "test.py"
    p_script = "python3 ./test.py"

    if not is_running(p_name):
        print("程序(%s)已挂,准备重启" % p_name)
        if not is_restart(p_script):
            is_restart(p_script)  # 再给一次机会

def main():
    # 信号处理
    signal.signal(signal.SIGALRM, heartbeat)
    # 第一次1s后检查,以后每5s检查一次
    signal.setitimer(signal.ITIMER_REAL, 1, 5)
    while True:
        time.sleep(5)  # 不用担心影响signal(优先级别高)

if __name__ == '__main__':
    main()

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
程序(test.py)已挂,准备重启
[PID:7270]进程运行中...
Aborted (core dumped)
程序(test.py)已挂,准备重启
[PID:7278]进程运行中...
Aborted (core dumped)
[PID:7284]进程运行中...
.....
正规流程的守护进程

写了个伪牌子的,现在说说正规的,看看概念的东西:

特点

  1. 后台服务进程
  2. 脱离于控制终端(setpid)
  3. 周期性的执行某个任务|等待某个事件发生(setitimer)
  4. 不受用户登录注销影响(关机影响,不过你可以添加启动项)
  5. 一般使用以d结尾的服务名称(约定俗成)

讲正式流程前先复习一下上面说的 进程组会话

  1. 进程组:每一个进程都属于一个“进程组”,当一个进程被创建的时候,它默认是其父进程所在组的成员(你们一家
  2. 会 话:几个进程组又构成一个会话(你们小区

需要扩充几点:

  1. 进程组
    1. 组长:第一个进程
    2. 组长ID==进程组ID
    3. 组长挂了不影响进程组
  2. 会话
    1. 组长不能创建会话(你都有官了,不留点门路给后人?)
    2. 创建会话的进程成为新进程组的组长(新进程组里面就它一个嘛)
    3. 创建出新会话会丢弃原有的控制终端(到了新环境里面,人脉得重新建立)

稍微验证一下,然后步入正题:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time

def main():
    pid = os.fork()
    if pid == 0:
        for i in range(7):
            print("子进程:PID=%d,PPID=%d,PGrpID=%d" % (os.getpid(), os.getppid(), os.getpgrp()))
            time.sleep(i)
    elif pid > 0:
        print("父进程:PID=%d,PPID=%d,PGrpID=%d" % (os.getpid(), os.getppid(), os.getpgrp()))
        time.sleep(4)

    print("遗言:PID=%d,PPID=%d,PGrpID=%d" % (os.getpid(), os.getppid(), os.getpgrp()))

if __name__ == '__main__':
    main()

验证结果: 父进程ID==进程组ID父进程挂了进程组依旧在,顺便验证了下 ps-ajx的参数

先看看这个SessionID是啥:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time

def main():
    print("进程:PID=%d,PPID=%d,PGrpID=%d" % (os.getpid(), os.getppid(), os.getpgrp()))
    print(os.getsid(os.getpid()))
    for i in range(1, 5):
        time.sleep(i)
    print("over")

if __name__ == '__main__':
    main()

ps ajx的参数现在全知道了:PPID PID PGID SID (你不加grep就能看到的)

验证一下SessionID的事情:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 验证一下父进程不能创建会话ID
import os

def main():
    pid = os.getpid()
    print("进程:PPID=%d,PID=%d,GID=%d,SID=%d" % (pid, os.getppid(), os.getpgrp(),os.getsid(pid)))
    os.setsid() # 父进程没法设置为会话ID的验证


if __name__ == '__main__':
    main()
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
进程:PPID=3301,PID=2588,GID=3301,SID=3301



---------------------------------------------------------------------------

PermissionError                           Traceback (most recent call last)

<ipython-input-1-375f70009fcf> in <module>()
      8 
      9 if __name__ == '__main__':
---> 10     main()


<ipython-input-1-375f70009fcf> in main()
      4     pid = os.getpid()
      5     print("进程:PPID=%d,PID=%d,GID=%d,SID=%d" % (pid, os.getppid(), os.getpgrp(),os.getsid(pid)))
----> 6     os.setsid() # 父进程没法设置为会话ID的验证
      7 
      8 


PermissionError: [Errno 1] Operation not permitted

步入正轨:

创建守护进程的步骤

  1. fork子进程,父进程退出(子进程变成了孤儿)
  2. 子进程创建新会话(创建出新会话会丢弃原有的控制终端)
  3. 改变当前工作目录【为了减少bug】(eg:你在某个文件夹下运行,这个文件夹被删了,多少会点受影响)
  4. 重置文件掩码(继承了父进程的文件掩码,通过 umask(0)重置一下,这样可以获取777权限)
  5. 关闭文件描述符(既然用不到了,就关了)
  6. 自己的逻辑代码

先简单弄个例子实现上面步骤:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
from sys import stdin, stdout, stderr

def main():

    # 【必须】1. fork子进程,父进程退出(子进程变成了孤儿)
    pid = os.fork()
    if pid > 0:
        exit(0)

    # 【必须】2. 子进程创建新会话(创建出新会话会丢弃原有的控制终端)
    os.setsid()

    # 3. 改变当前工作目录【为了减少bug】# 改成不会被删掉的目录,比如/
    os.chdir("/home/dnt")  # 我这边因为是用户创建的守护进程,就放它下面,用户删了,它也没必要存在了

    # 4. 重置文件掩码(获取777权限)
    os.umask(0)

    # 5. 关闭文件描述符(如果写日志也可以重定向一下)
    os.close(stdin.fileno())
    os.close(stdout.fileno())
    os.close(stderr.fileno())

    # 【必须】6. 自己的逻辑代码
    while True:
        time.sleep(1)

if __name__ == '__main__':
    main()

运行效果:(直接后台走起了)


基础回顾

如果对Linux基础不熟,可以看看几年前说的LinuxBase:

Linux基础命令:http://www.cnblogs.com/dunitian/p/4822807.html

Linux系列其他文章:https://www.cnblogs.com/dunitian/p/4822808.html#linux


如果对部署运行系列不是很熟,可以看之前写的小demo:

用Python3、NetCore、Shell分别开发一个Ubuntu版的定时提醒(附NetCore跨平台两种发布方式):https://www.cnblogs.com/dotnetcrazy/p/9111200.html


如果对OOP不是很熟悉可以查看之前写的OOP文章:

Python3 与 C# 面向对象之~封装https://www.cnblogs.com/dotnetcrazy/p/9202988.html

Python3 与 C# 面向对象之~继承与多态https://www.cnblogs.com/dotnetcrazy/p/9219226.html

Python3 与 C# 面向对象之~异常相关https://www.cnblogs.com/dotnetcrazy/p/9219751.html


如果基础不牢固,可以看之前写的PythonBase:

Python3 与 C# 基础语法对比(Function专栏)https://www.cnblogs.com/dotnetcrazy/p/9175950.html

Python3 与 C# 扩展之~模块专栏https://www.cnblogs.com/dotnetcrazy/p/9253087.html

Python3 与 C# 扩展之~基础衍生https://www.cnblogs.com/dotnetcrazy/p/9278573.html

Python3 与 C# 扩展之~基础拓展https://www.cnblogs.com/dotnetcrazy/p/9333792.html


现在正儿八经的来个简化版的守护进程:(你可以根据需求多加点信号处理)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import os
import time
import signal
from sys import stdin, stdout, stderr

class Daemon(object):
    def __init__(self, p_name, p_script):
        self.p_name = p_name
        self.p_script = p_script

    @staticmethod
    def write_log(msg):
        # 追加方式写
        with open("info.log", "a+") as f:
            f.write(msg)
            f.write("\n")

    def is_running(self, p_name):
        """是否在运行"""
        try:
            # grep -v grep 不显示grep本身,wc -l是计数用的
            result = os.popen(
                "ps ax | grep %s | grep -v grep" % p_name).readlines()
            if len(result) > 0:
                return True
            else:
                return False
        except Exception as ex:
            self.write_log(ex)
            return False

    def is_restart(self, p_script):
        """重启程序"""
        try:
            if os.system(p_script) == 0:
                return True
            else:
                return False
        except Exception as ex:
            self.write_log(ex)
            return False

    def heartbeat(self, signalnum, frame):
        """心跳检查"""
        if not self.is_running(self.p_name):
            self.write_log("[%s]程序(%s)已挂,准备重启" % (time.strftime("%Y-%m-%d %X"),
                                                  self.p_name))
            if not self.is_restart(self.p_script):
                self.is_restart(self.p_script)  # 再给一次机会

    def run(self):
        """运行守护进程"""
        pid = os.fork()
        if pid > 0:
            exit(0)

        os.setsid()  # 子进程创建新会话
        os.chdir("/home/dnt")  # 改变当前工作目录
        os.umask(0)  # 获取777权限

        # 5. 关闭文件描述符
        os.close(stdin.fileno())
        os.close(stdout.fileno())
        os.close(stderr.fileno())

        # 【必须】6. 自己的逻辑代码
        # 捕捉设置的定时器
        signal.signal(signal.SIGALRM, self.heartbeat)
        # 第一次2s后执行,以后5s执行一次
        signal.setitimer(signal.ITIMER_REAL, 2, 5)

        self.write_log("[%s]daeman running" % time.strftime("%Y-%m-%d %X"))
        self.write_log("p_name:%s,p_script:%s" % (self.p_name, self.p_script))

        while True:
            time.sleep(5)  # 不用担心影响signal(优先级别高)

def main():
    try:
        pro = Daemon("test.py", "python3 ~/demo/test.py")
        pro.run()
    except Exception as ex:
        Daemon.write_log(ex)

if __name__ == '__main__':
    main()

运行效果:(关闭文件描述符后就不要printf了)

扩展说明,如果你要文件描述符重定向的话可以这么写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
with open("in.log", "a+") as f:
    os.dup2(f.fileno(), sys.stdin.fileno())
with open("out.log", "a+") as f:
    os.dup2(f.fileno(), sys.stdout.fileno())
with open("err.log", "a+") as f:
    os.dup2(f.fileno(), sys.stderr.fileno())

之后你printf就自动到指定的文件了

扩展说明:

Socket,在讲基础最后一个系列~网络编程的时候会讲,不急,而且进程间通信不需要这么 ‘重量级’

线程相关打算和代码一起讲,有机会也可以单独拉出来说一个结尾篇


业余拓展:

官方文档大全

进程间通信和网络

os - 其他操作系统接口

mmap - 内存映射文件支持

signal - 设置异步事件的处理程序

Other:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Linux下012号进程
https://blog.csdn.net/gatieme/article/details/51484562
https://blog.csdn.net/gatieme/article/details/51532804
https://blog.csdn.net/gatieme/article/details/51566690

Linux 的启动流程
http://www.ruanyifeng.com/blog/2013/08/linux_boot_process.html
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html

孤儿进程与僵尸进程
https://www.cnblogs.com/Anker/p/3271773.html
https://blog.csdn.net/believe_s/article/details/77040494

Python2 OS模块之进程管理
https://www.cnblogs.com/now-fighting/p/3534185.html

缓冲区的个人理解
https://blog.csdn.net/lina_acm/article/details/51865543

深入Python多进程编程基础
https://zhuanlan.zhihu.com/p/37370577
https://zhuanlan.zhihu.com/p/37370601

python多进程实现进程间通信实例
https://www.jb51.net/article/129016.htm

PIPE2参考:
https://bugs.python.org/file22147/posix_pipe2.diff
https://stackoverflow.com/questions/30087506/event-driven-system-call-in-python
https://stackoverflow.com/questions/5308080/python-socket-accept-nonblocking/5308168

FIFO参考:
https://blog.csdn.net/an_tang/article/details/68951819
https://blog.csdn.net/firefoxbug/article/details/8137762

Python之mmap内存映射模块(大文本处理)说明
https://www.cnblogs.com/zhoujinyi/p/6062907.html

python 基于mmap模块的jsonmmap实现本地多进程内存共享
https://www.cnblogs.com/dacainiao/p/5914114.html

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。
https://blog.csdn.net/Android_Mrchen/article/details/77866490

事务四大特征:原子性,一致性,隔离性和持久性
https://blog.csdn.net/u014079773/article/details/52808193

python 、mmap 实现内存数据共享
https://www.jianshu.com/p/c3afc0f02560
http://www.cnblogs.com/zhoujinyi/p/6062907.html
https://blog.csdn.net/zhaohongyan6/article/details/71158522

Python信号相关:
https://my.oschina.net/guol/blog/136036

Linux--进程组、会话、守护进程
https://www.cnblogs.com/forstudy/archive/2012/04/03/2427683.html
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我为Net狂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
[Android] 查看MTD,EMMC,MMC三种设备的分区
因为在做系统升级,AOSP的recovery下有一个flash_image工具,这个工具可以在开机状态下刷写系统分区。源码位置在/bootable/recovery/mtdutils/flash_image.c。
wOw
2020/01/20
6.5K0
[Android] 查看MTD,EMMC,MMC三种设备的分区
安卓手机运行 Windows 操作系统:一
在折腾上一篇文章的时候,发现了一条有趣的折腾分支,在这台老设备上运行 Windows 操作系统。
soulteary
2024/05/04
5610
安卓手机运行 Windows 操作系统:一
安卓手机运行 Windows 操作系统:一
在折腾上一篇文章的时候,发现了一条有趣的折腾分支,在这台老设备上运行 Windows 操作系统。
soulteary
2024/05/11
5890
安卓手机运行 Windows 操作系统:一
Linux U-Boot 开发指南
介绍 U-Boot 的编译打包、基本配置、常用命令的使用、基本调试方法等, 为 U-BOOT 的移植及应用开发提供了基础。
韦东山
2023/02/25
4.8K0
Linux U-Boot 开发指南
嵌入式Linux开发-uboot常用命令介绍(上篇)
U-boot是一种开源bootloader, 作用是用来引导操作,以及给开发人员提供测试调试工具。本身算是个精简的Linux系统,主要是负责硬件的初始化和引导,本身带有一些工具,作为引导程序,常作为嵌入式设备的引导。当真正的系统开始运行的时候U-boot就把管理权限交了出去。
DS小龙哥
2022/04/08
2.1K0
嵌入式Linux开发-uboot常用命令介绍(上篇)
Nexus5 bootloader内容初探及延伸思考编译使用
在Android启动过程分析-从按下电源键到第一个用户进程[转载]中,我们知道BootLoader是在操作系统前执行的程序,有没有很好奇它到底有些啥内容呢?
用户2930595
2018/08/23
5080
学习下 eMMC
eMMC 简介 Host Interface Flash Controller Flash Memory eMMC 分区管理 Boot Area Partitions eMMC 分区应用实例 eMMC 总线协议 eMMC 总线接口 eMMC 总线模型 eMMC 简介 eMMC 是 embedded MultiMediaCard 的简称。MultiMediaCard,即MMC, 是一种闪存卡(Flash Memory Card)标准,它定义了 MMC 的架构以及访问 Flash Memory 的接口和协议。而
刘盼
2022/04/06
2.1K0
学习下 eMMC
Rockchip RK3588 - uboot引导方式介绍
开发板 :RK3588 EVB开发板 eMMC :256GB LPDDR4 :16GB 显示屏 :10.1英寸HDMI接口显示屏 u-boot :2017.09 linux :6.1
用户11537198
2025/03/07
5100
Mi8刷机若干踩坑
这个刷机的事情本来挺简单个事,但是我刷面具的时候,哪个面具是个坏包,之后的就触摸不正常了,就是点上去没有反应.很难受我又刷回去了miui然后再刷回来.本来是半个小时的事情,结果折腾一下午,晚上一点多才搞好.
云深无际
2020/08/12
1.8K0
Mi8刷机若干踩坑
嵌入式Linux开发-uboot常用命令介绍(下篇)
主要是涉及的命令是:磁盘分区、磁盘文件加载、内核引导、二进制文件加载、跳转命令、磁盘文件系统格式等等。
DS小龙哥
2022/04/08
2.7K0
嵌入式Linux开发-uboot常用命令介绍(下篇)
修改手机的ro.debuggable配置0x01:获取boot.img0x02:修改boot.img0x03:boot.img相关0x04:变砖后的自救
我们自己编译的源码刷机ro.debuggable就是为1,这个时候所有的进程都可以远程调试。但是自己编译的源码里面没有GooglePlay,有些应用的部分功能又依赖于GooglePlay。那怎么办呢?
用户2930595
2018/10/09
2.7K0
修改手机的ro.debuggable配置0x01:获取boot.img0x02:修改boot.img0x03:boot.img相关0x04:变砖后的自救
Android Recovery升级原理
Recovery模式指的是一种可以对安卓机内部的数据或系统进行修改的模式(类似于windows PE或DOS)。也可以称之为安卓的恢复模式,在这个所谓的恢复模式下,我们可以刷入新的安卓系统,或者对已有的系统进行备份或升级,也可以在此恢复出厂设置(格式化数据和缓存)。
233333
2019/09/25
5.1K1
Android Recovery升级原理
鸿蒙系统研究之六:U-Boot引导
U-Boot 的全称是 Universal Boot Loader,其作用就是引导系统。对于我们熟悉的 PC,上电后,通过 BIOS 引导操作系统 (Windows、Linux等)。对于嵌入式系统一般将这个引导程序称作 BootLoader,U-Boot 就是目前使用得最广泛的 BootLoader。
云水木石
2021/08/06
2.9K1
Android5.0 Recovery源代码分析与定制(一)
Android的系统升级,Android的线刷,卡刷,格式化究竟有什么奇妙的地方呢?它又是怎么实现的呢?今天我将为大家揭开它的面纱!我们以Tiny4412的Recovery源代码为例,虽然4412并没有支持Recovery系统,但弄明白它的原理,我们也可以开发或者移植一个出来。其实,在recovery.cpp中开头就已经做了详细的说明,我们来看看。
杨源鑫
2019/07/04
1.7K0
Android5.0 Recovery源代码分析与定制(一)
Android加密之全盘加密
Android 的安全性问题一直备受关注,Google 在 Android 系统的安全方面也是一直没有停止过更新,努力做到更加安全的手机移动操作系统。
全栈程序员站长
2022/08/27
2.6K0
Android加密之全盘加密
14_TF编程
​ 多媒体存储卡(英语:Multimedia Card),又译MMC卡,是一种快闪记忆卡标准。在1997年由西门子及闪迪共同开发,技术基于东芝的NAND快闪记忆技术,因此较早期基于Intel NOR快闪记忆技术的存储卡(例如CF卡)更细小。MMC卡大小与一张邮票差不多,约24mm x 32mm x 1.5mm。MMC卡原本使用1bit串联接口,但较新的标准则容许同时发送4 bit或8 bits的数据。近年MMC卡技术已差不多完全被SD卡所代替;但由于MMC卡仍可被兼容SD卡的设备所读取,因此仍有其作用。这项技术一个公开标准,所有愿意改进它或者为它开发产品的公司都可使用。
韦东山
2022/05/05
1.7K0
14_TF编程
android adb shell 常用命令
mac: /Users/xx/Library/Android/sdk/tools/bin archquery jobb monkeyrunner sdkmanager avdmanager lint screenshot2 uiautomatorviewer
tea9
2022/09/08
3.6K0
Android Automotive Framework调试技巧
三次握手只是一个数据传输的过程,但是,我们传输前需要一些准备工作,比如将创建一个套接字,收集一些计算机的资源,将一些资源绑定套接字里面,以及接受和发送数据的函数等等,这些功能接口在一起构成了socket的编程
wizzie
2022/12/22
5.5K0
Android Automotive Framework调试技巧
私人订制属于自己的Linux系统
init通过调用/etc/inittab这个配置文件,然后再去执行/etc/rc.d/rc.sysinit的系统初始化脚本
iginkgo18
2020/09/27
1.1K0
私人订制属于自己的Linux系统
Tina_Linux_安全_开发指南
介绍TinaLinux 下安全方案的功能。安全完整的方案基于normal 方案扩展,覆盖硬件安全、安全启动(Secure Boot)、安全系统(Secure OS)、安全存储(Secure Storage)、安全应用(Trust Application)、完整性保护(Dm-Verity)、强制访问控制(MAC)等方面。
韦东山
2023/02/25
5.9K0
Tina_Linux_安全_开发指南
相关推荐
[Android] 查看MTD,EMMC,MMC三种设备的分区
更多 >
LV.1
华为全栈工程师
目录
  • 2.4.5.进程间通信~MMAP内存映射(常用)
    • 有血缘关系使用MMAP通信
    • 无血缘关系使用MMAP通信
  • 2.4.6.进程间通信~Signal信号
    • 理论开始
    • 信号捕捉
    • 定时器alarm(执行一次)
    • 普及一个小技巧
    • 定时器setitimer(周期执行)
  • 2.4.7.进程守护
    • 正规流程的守护进程
    • 基础回顾
  • 扩展说明:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档