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

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

  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为例,简单解析一下帮助文档:(加粗的是必填参数)

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(和文件一致)
# 这个够明了了,\0转换成二进制就是\x00
"\0".encode()
b'\x00'
# 老规矩,开始之前,扩充一个小知识点:(空字符串和'\0'区别)
a = "" # 空字符串 (Python里面没有char类型)
b = "\x00" # `\0` 的二进制写法
c = "\0"

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

print(len(a))
print(len(b))
print(len(c))
0
1
1

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

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循环退出条件

熟悉一下上面几个方法:

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)

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打开的案例:

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()

输出:

postion_index:0
小明同学最爱刷碗
postion_index:25
小明同学最爱刷碗
小潘同学最爱打扫
postion_index:25
postion_index:0

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

注意一点:

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

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

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

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()

输出:

[父进程]PID:6843,PPID:3274
[子进程]PID:6844,PPID:6843
子进程说:老爸,我想出去玩了~
父进程说:去吧去吧
[父进程]收尸:PID:6844,Status:0

有血缘关系使用MMAP通信

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

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()

输出:

[父进程]PID:8115,PPID:3274
[子进程]PID:8116,PPID:8115
[子进程]老爸我出去嗨了~
[父进程]去吧,皮卡丘~
[父进程]收尸:PID:8116,Status:0

无血缘关系使用MMAP通信

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

进程1:

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:

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

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()

输出:

[父进程]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

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)

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)【有权限的都杀了】

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()

输出:

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后的子进程继承

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()

输出

[父进程]PID=9687,PPID=9063
[遗言]PID=9687,PPID=9063
[子进程]PID=9688,PPID=9687
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[子进程]孩子老卵,怎么滴吧~
[遗言]PID=9688,PPID=1060

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

普及一个小技巧

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

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信号给进程。

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

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()

输出:

我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
我会周期性执行哦~
我在做其他事情
我在做其他事情
我在做其他事情
...

2.4.7.进程守护

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

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

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

模拟一个漏洞百出的程序

import os
import time

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

if __name__ == '__main__':
    main()

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

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()

输出:

程序(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. 创建出新会话会丢弃原有的控制终端(到了新环境里面,人脉得重新建立)

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

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是啥:

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的事情:

# 验证一下父进程不能创建会话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()
进程: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. 自己的逻辑代码

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

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


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

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了)

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

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:

Linux下0、1、2号进程
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

原文发布于微信公众号 - 我为Net狂(dotNetCrazy)

原文发表时间:2018-07-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逸鹏说道

1.并发编程~先导篇(上)

并发 :一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

1293
来自专栏小樱的经验随笔

【批处理学习笔记】第二十二课:系统变量

    批处理的一些变量是由操作系统事先定义好的,可以适用于任何批处理,我们称这些特殊的变量为“系统变量”。系统变量有很多个,包括硬件类、操作系统类、文件路径类...

2814
来自专栏liulun

基于.net开发chrome核心浏览器【七】

这是一个系列的文章,前面六篇文章的地址如下: 基于.net开发chrome核心浏览器【六】 基于.net开发chrome核心浏览器【五】 基于...

3637
来自专栏逆向与安全

Xposed截获 Android手机QQ密码

   Xposed框架是一款修改系统框架服务的软件,通过它许多功能强大的模块得以实现,且不冲突地同时运作,自从Xposed框架发布以来,安卓手机的可玩性日益激增...

1260
来自专栏琯琯博客

awesome-php-cn软件资源

PHP 资源列表,内容包括:库、框架、模板、安全、代码分析、日志、第三方库、配置工具、Web 工具、书籍、电子书、经典博文等。 依赖管理 依赖和包管理库 Com...

4105
来自专栏FreeBuf

软件漏洞分析技巧分享

作者:riusksk【TSRC】 在日常分析软件漏洞时,经常需要耗费比较长的分析时间,少则几小时,多则数天,甚至更久。因此,经常总结一些分析技巧是非常有必要的,...

2469
来自专栏逍遥剑客的游戏开发

从Native到Web(二), NaCl学习笔记: 技术限制&Win32移植过程

1342
来自专栏逸鹏说道

SQLServer执行命令出现“目录无效的提示”

异常处理汇总-数据库系列 http://www.cnblogs.com/dunitian/p/4522990.html ? 一般都是清理垃圾清理过头了,把不该...

3837
来自专栏逸鹏说道

【推荐】C#线程篇---你所不知道的线程池(4)

线程的创建和销毁都要耗费大量的时间,有什么更好的办法?用线程池! 太多的线程浪费内存资源,有什么更好的办法?用线程池! 太多线程有损性能,有什么更好的办法?用线...

3638
来自专栏宏伦工作室

全栈 - 3 序言 带好装备Python和Sublime

1834

扫码关注云+社区

领取腾讯云代金券