从Redis中的BGSAVE命令谈起Fork—之一

来源:互联网 发布:采购软件 编辑:程序博客网 时间:2024/06/03 08:07

引言
本人近日在读黄建宏先生的《Redis设计与实现》中RDB文件的创建与载入一节,了解到SAVE命令和BGSAVE命令的实现。
SAVE:其中SAVE命令是阻塞式的,它会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE:和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(即父进程)继续处理命令请求。
本文由BGSAVE命令的实现谈起Python/OS模块中的fork函数。为了更加清晰的了解fork函数的工作原理,读者可以跟我一块在linux下查看进程(ps命令),并对这个方法做一些测试,加深理解。(需要注意的是fork这个函数和操纵系统本身结合的非常紧密,windows下无法使用os.fork()函数,必须在POSIX系统(如Liunx/Unix/Mac系统)下进行测试

第一部分
首先,我们来看一下,BGSAVE命令的实现,下面以Python语言给出其实现伪代码:

def BGSAVE():    #创建子进程    pid = fork()    if pid == 0:        #子进程负责创建RDB文件        rdbSave()        #完成之后向父进程发出信号        signal_parent()    elif pid > 0:        #父进程继续处理命令请求,并通过轮询等待子进程信号        handle_request_and_wait_signal()    else:        #处理出错情况        handle_fork_error()

读者如果不了解fork函数或者说Python中多进程的创建,读到这段代码可能有些不太明白,没关系,请看下面(简单直接)的解释。

python中的os.fork()被调用后就会立即生成一个子进程,是通过copy父进程的地址空间和资源来实现子进程的创建,同时:

  • fork函数在子进程中返回的是0;
  • 在父进程中返回的是子进程的PID;
  • 出现错误,fork返回一个负值;

所以,我们可以通过fork的返回值来判断当前进程是子进程还是父进程。现在再返回去看这个程序是不是就很清楚了,没理解也没关系,下面进行详细的讨论。


第二部分
首先我们来写一个简单的Python脚本,这个程序运行时,系统会生成一个新的进程,看下面代码:

#!/usr/bin/env python#coding=utf8from time import sleepprint "Main thread!"sleep(3000)

因为代码执行完后,进程就会被销毁,所以这里让程序休眠30秒,方便看到效果。在linux下执行这个代码:

python hello.py &

加上&符号,可以让程序在后台运行,不会占用终端。输入ps -l命令查看进程,在终端输出如下:
这里写图片描述
其中第二条记录就是刚才运行的python程序。
接着,我们使用fork来创建子进程
使用fork创建一个新进程成功后,新进程会是原进程的子进程,原进程称为父进程。如果发生错误,则会抛出OSError异常。

#!/usr/bin/env python#coding=utf8from time import sleepimport ostry:    pid = os.fork()except OSError, e:    passsleep(30)

运行代码,查看进程,在终端上输出如下:
这里写图片描述
由图知,运行代码后产生了两条进程,可以看出第二条python进程就是第一条的子进程。
然后,我们再来看fork进程后的程序流程
使用fork创建子进程后,子进程会复制父进程的数据信息,而后程序就分两个进程继续运行后面的程序,这也是fork(分叉)名字的含义了。在子进程内,这个方法会返回0;在父进程内,这个方法会返回子进程的编号PID。可以使用PID来区分两个进程(这个程序就类似于文章开始时候给出的BGSAVE的实现伪代码):

#!/usr/bin/env python#coding=utf8import os#创建子进程之前声明的变量source = 10try:    pid = os.fork()    if pid == 0: #子进程        print "this is child process."        #在子进程中source自减1        source = source - 1        sleep(3)    else: #父进程        print "this is parent process."    print sourceexcept OSError, e:    pass

上面代码中,在子进程创建前,声明了一个变量source,然后在子进程中自减1,最后打印出source的值,显然父进程打印出来的值应该为10,子进程打印出来的值应该为9。为了明显区分父进程和子进程,让子进程睡3秒,就看的比较明显了。
既然子进程是父进程创建的,那么父进程退出之后,子进程会怎么样呢?此时,子进程会被PID为1的进程接管,就是init进程了。这样子进程就不会受终端退出影响了,使用这个特性就可以创建在后台执行的程序,俗称守护进程(daemon)。


第三部分
虽然fork给予多进程编程很大的方面性,但是滥用也是会有很多大问题的,如果程序代码中有fork子进程来操作数据,但是由于fork之后,没有及时的退出,就会导致系统中的Python进程越来越多,子进程越来越多。请看Python下fork进程的测试代码:

def fork(a):    def now():        import datetime        return datetime.datetime.now().strftime("%S.%f")    import os    import time    print now(), a    if os.fork() == 0:        print '子进程[%s]:%s' % (now(), os.getpid())        while 1:            a-=10            print '子进程的a值[%s]:%s' % (now(), a)            if a < 1:                break        print '准备退出子进程'        #os._exit(0) ## 你可以在这里退出子进程    else:        print '父进程[%s]:%s' % (now(), os.getpid())        while 1:            a-=1            print '父进程的a值[%s]:%s' % (now(), a)            if a < 0:                break        time.sleep(1)        print '等待子进程结束...'        try:            result = os.wait()            if result:                print '子进程:', result[0], result[1]            else:                print '没有数据!'        except:            print '异常...'        print '父进程...'    print '最后的值:',a    #exit(0)  ## 你也可以在这里退出,注意,这里是父进程和子进程都共用的地方,在这里退出会导致父进程也一并退出

随后的TIPS:
os.fork() 会有两次返回值,分别是父进程和子进程的返回值
在父进程中,fork返回的值是子进程的PID;
子进程中,这个返回值为0
子进程会复制父进程的上下文
父子进程并不能确定执行顺序
os.fork()之后,子进程一定要使用exit()或者os._exit()来退出子进程环境,建议使用os._exit()。

0 0
原创粉丝点击