Python 中的 os.popen 函數(shù) 與 Pipe 管道的坑

image

前言

最近用 Python 寫了幾個(gè)簡(jiǎn)單的腳本來處理一些數(shù)據(jù),因?yàn)橹皇呛?jiǎn)單功能所以我就直接使用 print 來打印日志凳兵。

任務(wù)運(yùn)行時(shí)偶爾會(huì)出現(xiàn)一些異常:

image

因?yàn)槲以诓煌胤蕉加写蛴∪罩韭拱裕瑢?dǎo)致每次報(bào)錯(cuò)的地方都不太一樣柄延,從而導(dǎo)致程序運(yùn)行結(jié)果非常詭異肃廓;有時(shí)候是這段代碼沒有運(yùn)行翻屈,下一次就可能是另外一段代碼沒有觸發(fā)陈哑。

雖說當(dāng)時(shí)有注意到 Broken pipe 這個(gè)關(guān)鍵異常,但沒有特別在意伸眶,因?yàn)榇a中也有一些發(fā)送 http 請(qǐng)求的地方惊窖,一直以為是網(wǎng)絡(luò) IO 出現(xiàn)了問題,壓根沒往 print 這個(gè)最基本的打印函數(shù)上思考??厘贼。

直到這個(gè)問題反復(fù)出現(xiàn)我才認(rèn)真看了這個(gè)異常界酒,定睛一看 print 不也是 IO 操作嘛,難道真的是自帶的 print 函數(shù)都出問題了嘴秸?


但在本地毁欣、測(cè)試環(huán)境我運(yùn)行無數(shù)次也沒能發(fā)現(xiàn)異常;于是我找運(yùn)維拿到了線上的運(yùn)行方式岳掐。

原來為了方便維護(hù)大家提交上來的腳本任務(wù)凭疮,運(yùn)維自己有維護(hù)一個(gè)統(tǒng)一的腳本,在這個(gè)腳本中使用:

cmd = 'python /xxx/test.py'
os.popen(cmd)

來觸發(fā)任務(wù)串述,這也是與我在本地执解、開發(fā)環(huán)境的唯一區(qū)別。

popen 原理

為此我在開發(fā)環(huán)境模擬出了異常:

test.py:

import time
if __name__ == '__main__':
    time.sleep(20)
    print '1000'*1024

task.py:

import os
import time
if __name__ == '__main__':
    start = int(time.time())
    cmd = 'python test.py'
    os.popen(cmd)
    end = int(time.time())
    print 'end****{}s'.format(end-start)

運(yùn)行:

python task.py

等待 20s 必然會(huì)復(fù)現(xiàn)這個(gè)異常:

Traceback (most recent call last):
  File "test.py", line 4, in <module>
    print '1000'*1024
IOError: [Errno 32] Broken pipe

為什么會(huì)出現(xiàn)這個(gè)異常呢纲酗?

首先得了解 os.popen(command[, mode[, bufsize]]) 這個(gè)函數(shù)的運(yùn)行原理衰腌。

image

根據(jù)官方文檔的解釋,該函數(shù)會(huì)執(zhí)行 fork 一個(gè)子進(jìn)程執(zhí)行 command 這個(gè)命令觅赊,同時(shí)將子進(jìn)程的標(biāo)準(zhǔn)輸出通過管道連接到父進(jìn)程右蕊;

也就該方法返回的文件描述符。

這里畫個(gè)圖能更好地理解其中的原理:


image

在這里的使用場(chǎng)景中并沒有獲取 popen() 的返回值茉兰,所以 command 的執(zhí)行本質(zhì)上是異步的尤泽;

也就是說當(dāng) task.py 執(zhí)行完畢后會(huì)自動(dòng)關(guān)閉讀取端的管道。

image

如圖所示,關(guān)閉之后子進(jìn)程會(huì)向 pipe 中輸出 print '1000'*1024坯约,由于這里輸出的內(nèi)容較多會(huì)一下子填滿管道的緩沖區(qū)熊咽;

于是寫入端會(huì)收到 SIGPIPE 信號(hào),從而導(dǎo)致 Broken pipe 的異常闹丐。

從維基百科中我們也可以看出這個(gè)異常產(chǎn)生的一些條件:


image

其中也提到了 SIGPIPE 信號(hào)横殴。

解決辦法

既然知道了問題原因,那解決起來就比較簡(jiǎn)單了卿拴,主要有以下幾個(gè)方案:

  1. 使用 read() 函數(shù)讀取管道中的數(shù)據(jù)衫仑,全部讀取之后再關(guān)閉。
  2. 如果不需要子進(jìn)程中的輸出時(shí)堕花,也可以將 command 的標(biāo)準(zhǔn)輸出重定向到 /dev/null文狱。
  3. 也可以使用 Python3subprocess.Popen 模塊來運(yùn)行。

這里使用第一種方案進(jìn)行演示:

import os
import time
if __name__ == '__main__':
    start = int(time.time())
    cmd = 'python test.py'
    with os.popen(cmd) as p:
        print p.read()
    end = int(time.time())
    print 'end****{}s'.format(end-start)
image

運(yùn)行 task.py 之后不會(huì)再拋異常缘挽,同時(shí)也將 command 的輸出打印出來湿酸。

線上修復(fù)時(shí)我沒有采用這個(gè)方案井赌,為了方便查看日志杠茬,還是使用標(biāo)準(zhǔn)的日志框架將日志輸出到了 es 中寸齐,方便統(tǒng)一在 kibana 中進(jìn)行查看。

由于日志框架并沒有使用到管道腮郊,所以自然也不會(huì)有這個(gè)問題摹蘑。

更多內(nèi)容

問題雖然是解決了,其中還是涉及到了一些咱們平時(shí)不太注意的知識(shí)點(diǎn)轧飞,這次我們就來一起回顧一下衅鹿。

首先是父子進(jìn)程的內(nèi)容,這個(gè)在 c/c++/python 中比較常見踪少,在 Java/golang 中直接使用多線程塘安、協(xié)程會(huì)更多一些。

比如這次提到的 Python 中的 os.popen() 就是創(chuàng)建了一個(gè)子進(jìn)程援奢,既然是子進(jìn)程那肯定是需要和父進(jìn)程進(jìn)行通信才能達(dá)到協(xié)同工作的目的兼犯。

很容易想到,父子進(jìn)程之間可以通過上文提到的管道(匿名管道)來進(jìn)行通信集漾。

還是以剛才的 Python 程序?yàn)槔星?dāng)運(yùn)行 task.py 后會(huì)生成兩個(gè)進(jìn)程:


image

分別進(jìn)入這兩個(gè)程序的/proc/pid/fd 目錄可以看到這兩個(gè)進(jìn)程所打開的文件描述符。

父進(jìn)程:

image

子進(jìn)程:

image

可以看到子進(jìn)程的標(biāo)準(zhǔn)輸出與父進(jìn)程關(guān)聯(lián)具篇,也就是 popen() 所返回的那個(gè)文件描述符纬霞。

這里的 0 1 2 分別對(duì)應(yīng)一個(gè)進(jìn)程的stdin(標(biāo)準(zhǔn)輸入)/stdout(標(biāo)準(zhǔn)輸出)/stderr(標(biāo)準(zhǔn)錯(cuò)誤)。

還有一點(diǎn)需要注意的是驱显,當(dāng)我們?cè)诟高M(jìn)程中打開的文件描述符诗芜,子進(jìn)程也會(huì)繼承過去瞳抓;

比如在 task.py 中新增一段代碼:

x = open("1.txt", "w")

之后查看文件描述符時(shí)會(huì)發(fā)現(xiàn)父子進(jìn)程都會(huì)有這個(gè)文件:


image

但相反的,子進(jìn)程中打開的文件父進(jìn)程是不會(huì)有的伏恐,這個(gè)應(yīng)該很容易理解孩哑。

總結(jié)

一些基礎(chǔ)知識(shí)在排查一些詭異問題時(shí)顯得尤為重要,比如本次涉及到的父子進(jìn)程的管道通信翠桦,最后來總結(jié)一下:

  1. os.popen() 函數(shù)是異步執(zhí)行的横蜒,如果需要拿到子進(jìn)程的輸出,需要自行調(diào)用 read() 函數(shù)销凑。
  2. 父子進(jìn)程是通過匿名管道進(jìn)行通信的丛晌,當(dāng)讀取端關(guān)閉時(shí),寫入端輸出到達(dá)管道最大緩存時(shí)會(huì)收到 SIGPIPE 信號(hào)斗幼,從而拋出 Broken pipe 異常澎蛛。
  3. 子進(jìn)程會(huì)繼承父進(jìn)程的文件描述符。

你的點(diǎn)贊與分享是對(duì)我最大的支持

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜕窿,一起剝皮案震驚了整個(gè)濱河市瓶竭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渠羞,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件智哀,死亡現(xiàn)場(chǎng)離奇詭異次询,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瓷叫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門屯吊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摹菠,你說我怎么就攤上這事盒卸。” “怎么了次氨?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵蔽介,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我煮寡,道長(zhǎng)虹蓄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任幸撕,我火速辦了婚禮薇组,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坐儿。我一直安慰自己律胀,他們只是感情好宋光,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炭菌,像睡著了一般罪佳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娃兽,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天菇民,我揣著相機(jī)與錄音,去河邊找鬼投储。 笑死第练,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的玛荞。 我是一名探鬼主播娇掏,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼勋眯!你這毒婦竟也來了婴梧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤客蹋,失蹤者是張志新(化名)和其女友劉穎塞蹭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讶坯,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡番电,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辆琅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漱办。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖婉烟,靈堂內(nèi)的尸體忽然破棺而出娩井,到底是詐尸還是另有隱情,我是刑警寧澤似袁,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布洞辣,位于F島的核電站,受9級(jí)特大地震影響叔营,放射性物質(zhì)發(fā)生泄漏屋彪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一绒尊、第九天 我趴在偏房一處隱蔽的房頂上張望畜挥。 院中可真熱鬧,春花似錦婴谱、人聲如沸蟹但。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽华糖。三九已至麦向,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間客叉,已是汗流浹背诵竭。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留兼搏,地道東北人卵慰。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像佛呻,于是被迫代替她去往敵國(guó)和親裳朋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容