我們通常會遇到這樣的需求:通過C++或其他較底層的語言實現了一個複雜的功能模塊,需要搭建一個基於Web的Demo,方法查詢數據。由於Python語言的強大和簡潔,其用來搭建Demo非常合適,Flask框架和jinja2模塊功能爲python提供了方便的web開發能力。同時,python能夠很方便的同其他語言的代碼交互。因此我們選擇python作爲開發Demo的工具。假設我們需要調用的模塊(提供底層服務)通過標準輸入循環讀入數據,處理完畢後把結果寫出到標出輸出,這樣的場景在Linux環境下很常見,依賴於Linux強大的重定向能力。然而,非常不幸的是,底層模塊有一個很重的初始化過程,因此我們不能夠每次查詢請求都去重新生成調用底層模塊的子進程。解決方案就是隻生成一次子進程,然後對每個請求通過管道(pipe)來和子進程交互。
Python的subprocess模塊可以很容易地生成子進程,類似Linux系統調用fork和exec。subprocess模塊的Popen對象可能以非阻塞的方式調用外部可執行程序,因此我們使用Poen對象來實現需求。如果我們想要把數據寫入子進程的標準輸入stdin,那麼在創建Popen對象的時候就需要指定參數stdin爲subprocess.PIPE;同樣,如果我們需要從子進程的標準輸出中讀取數據,那麼在創建Popen對象的時候就需要指定參數stdout爲subprocess.PIPE。先看一個簡單的例子:
1
2
3
|
from subprocess import Popen, PIPE p = Popen( 'less' , stdin = PIPE, stdout = PIPE) p.communicate( 'Line number %d.\n' % x) |
communicate函數返回一個二元組(stdoutdata, stderrdata),包含了子進程的標準輸出和標出錯誤的輸出數據。然而,由於Popen對象的communicate函數會阻塞父進程,同時還會關閉管道,因此每個Popen對象只能調用一次communicate函數,如果有多個請求必須重新生成Popen對象(重新初始化子進程),不能滿足我們的需求。
因此,我們只有往Popen對象的stdin和stdout對象裏寫入和讀取數據才能實現我們的需求。然而,不幸的是subprocess模塊默認情況下只運行在子進程結束的時候讀取一次標準輸出。Both subprocess and os.popen* only allow input and output one time, and the output to be read only when the process terminates.
進過一番研究之後我發現通過fcntl模塊的fcntl函數可以把子進程的標準輸出改爲非阻塞的方式,從而達到我們的目的。這樣困擾我許久的問題終於得到了完美解決。代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#!/usr/bin/python # -*- coding: utf-8 -*- # author: [email protected] from subprocess import Popen, PIPE import fcntl, os import time class Server( object ): def __init__( self , args, server_env = None ): if server_env: self .process = Popen(args, stdin = PIPE, stdout = PIPE, stderr = PIPE, env = server_env) else : self .process = Popen(args, stdin = PIPE, stdout = PIPE, stderr = PIPE) flags = fcntl.fcntl( self .process.stdout, fcntl.F_GETFL) fcntl.fcntl( self .process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) def send( self , data, tail = '\n' ): self .process.stdin.write(data + tail) self .process.stdin.flush() def recv( self , t = . 1 , e = 1 , tr = 5 , stderr = 0 ): time.sleep(t) if tr < 1 : tr = 1 x = time.time() + t r = '' pr = self .process.stdout if stderr: pr = self .process.stdout while time.time() < x or r: r = pr.read() if r is None : if e: raise Exception(message) else : break elif r: return r.rstrip() else : time.sleep( max ((x - time.time()) / tr, 0 )) return r.rstrip() if __name__ = = "__main__" : ServerArgs = [ '/home/weisu.yxd/QP/trunk/bin/normalizer' , '/home/weisu.yxd/QP/trunk/conf/stopfile.txt' ] server = Server(ServerArgs) test_data = '在雲端' , '雲梯' , '摩薩德' , 'Alisa' , 'iDB' , '阿里大數據' for x in test_data: server.send(x) print x, server.recv() |
另外,調用一些外部程序時,可能需要指定相應的環境變量,方式如下:
1
2
3
|
my_env = os.environ my_env[ "LD_LIBRARY_PATH" ] = "/path/to/lib" server = server.Server(cmd, my_env) |