简单队列系统RQ-深入理解(三)

318 阅读3分钟

存疑

RQ涉及一部分OS调用的代码,涉及进程和信号处理,如下代码引起了我的注意

def fork_and_perform_job(self, job):
    child_pid = os.fork()
    if child_pid == 0:
        self.main_work_horse(job)
    else:
        self._horse_pid = child_pid
        self.procline('Forked %d at %d' % (child_pid, time.time()))
        while True:
            try:
                os.waitpid(child_pid, 0)
                break
            except OSError as e:
                # In case we encountered an OSError due to EINTR (which is
                # caused by a SIGINT or SIGTERM signal during
                # os.waitpid()), we simply ignore it and enter the next
                # iteration of the loop, waiting for the child to end.  In
                # any other case, this is some other unexpected OS error,
                # which we don't want to catch, so we re-raise those ones.
                if e.errno != errno.EINTR:
                    raise

RQ在执行每一个job时都是fork一个子进程,让子进程进行处理,父进程调用waitpid等待子进程结束。代码中比较关键的点是except下面的注释,当产生SIGINT信号的时候会产生一个OSError,这个让我百思不得其解。

信号回顾

信号是进程间的一种通信方式,与其他进程间通信方式(例如管道、共享内存等)相比,信号所能传递的信息比较粗糙,只是一个整数。但正是由于传递的信息量少,信号也更便于管理和使用。

Python 中使用 signal 模块来处理信号相关的操作,定义如下:

signal.signal(signalnum, handler)
signal.getsignal(signalnum)

signalnum 为某个信号,handler 为该信号的处理函数。进程可以无视信号,可以采取默认操作,还可以自定义操作。当 handler 为 signal.SIG_IGN 时,信号被无视(ignore);当 handler 为 singal.SIG_DFL,进程采取默认操作(default);当 handler 为一个函数名时,进程采取函数中定义的操作。

  • 发送信号
os.kill(pid, sid)
os.killpg(pgid, sid)
  • 定时发信号
signal.alarm(2)

备注:多线程环境下使用信号,只有 main thread 可以设置 signal 的 handler,也只有它能接收到 signal。

  • 信号处理实践
import signal
import time

def register_signal_handlers():
    i=0
    def sigint_handler(signum, frame):
        nonlocal i
        if i>=1:
            exit()
        i+=1
        print("CTRL C")
        print(frame.f_code)
    
    j=0
    def alarm_handler(signum,frame):
        nonlocal j
        if j>=2:
            return
        else:
            print("时钟响了")
    signal.signal(signal.SIGINT,sigint_handler)
    signal.signal(signal.SIGALRM,alarm_handler)


def main():
    register_signal_handlers()
    signal.alarm(2)
    func=signal.getsignal(signal.SIGINT)
    while True:
        time.sleep(2)

if __name__=="__main__":
   main()

信号扩展

Python 信号处理程序不会在低级( C )信号处理程序中执行。相反,低级信号处理程序设置一个标志,告诉 virtual machine 稍后执行相应的 Python 信号处理程序(例如在下一个 bytecode 指令)。Python 信号处理程序总是会在主 Python 主解释器的主线程中执行,即使信号是在另一个线程中接收的。 这意味着信号不能被用作线程间通信的手段。

EINTR

  • 慢调用与EINTR:如果进程在一个慢系统调用(slow system call)中阻塞时,当捕获到某个信号且相应信号处理函数返回时,这个系统调用被中断,调用返回错误,设置errno为EINTR(相应的错误描述为Interrupted system call)。永远阻塞的系统调用是指调用永远无法返回,多数网络支持函数都属于这一类。如:若没有客户连接到服务器上,那么服务器的accept调用就会一直阻塞。

  • 处理方式:人为重启被中断的系统调用 或 忽略信号。

  • python处理EINTR:exception InterruptedError当系统调用被输入信号中断时将被引发。 对应于 errno EINTR。在 Python3.5更改: 当系统调用被某个信号中断时,Python 现在会重试系统调用,除非该信号的处理程序引发了其它异常 (原理参见 PEP 475) 而不是引InterruptedError。

  • pep475:标准库中提供的系统调用包装器在使用EINTR失败时应该自动重试,以减轻应用程序代码这样做的负担。如果信号处理程序成功返回,Python包装器将自动重试系统调用,python3.5以后的写法就简洁多了。

    ## python2写法
    while True:
        try:
            data = file.read(size)
            break
        except InterruptedError:
            continue
    
    ## python3.5写法
    while True:
         data = file.read(size)
    

相关链接