<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          請務(wù)必給 child_process 加上 on('data') 處理

          共 5688字,需瀏覽 12分鐘

           ·

          2020-09-17 03:55


          授權(quán)轉(zhuǎn)載自:Node地下鐵

          https://mp.weixin.qq.com/s/so7NyNNezgS3ZBEsh-UtZw

          好吧,我承認(rèn)我標(biāo)題黨了。其實(shí)里面有很多分支條件的,是 child_process 模塊中與 stdio 參數(shù)相關(guān)的函數(shù)需要加上 on('data') 事件處理。

          哪些與 stdio 相關(guān)呢?如 child_process.spawn()options 就有個(gè)可選參數(shù) stdio,你可以指定其為 inherit、pipe、ignore 等。

          怎么算加上 on('data') 事件處理呢?監(jiān)聽這個(gè)事件算一個(gè),將 stdio 指定為類似 ignore 這類操作也是算的。

          接下去我就以 child_process.spawn() 為例展開講吧。

          child_process.spawn(command[, args][, options])

          我們先來看看 child_process.spawn() 函數(shù):

          • command:要執(zhí)行的命令;
          • [,args]:執(zhí)行命令時(shí)的命令行參數(shù);
          • [,options]:擴(kuò)展選項(xiàng)。

          我們不關(guān)心前面的內(nèi)容,只關(guān)心 options 中的 stdio 屬性。

          options.stdio 可以是一個(gè)數(shù)組,也可以直接是一個(gè)字符串。

          如果 options.stdio 是一個(gè)數(shù)組,則它指定了子進(jìn)程對應(yīng)序號(hào)的 fd 應(yīng)該是什么。默認(rèn)不配置的情況下,spawn() 出來的子進(jìn)程對象(設(shè)為 child)中會(huì)有 child.stdin、child.stdoutchild.stderr 三個(gè) Stream 對象,而子進(jìn)程的 stdin、stdoutstderr 三個(gè) fd 會(huì)通過管道會(huì)被重定向到該三個(gè)流中——相當(dāng)于 options.stdio 配置了 'pipe'。

          如果 options.stdio 是一個(gè)字符串,則代表子進(jìn)程前三個(gè) fd 都是該字符串對應(yīng)的含義。如 'pipe'[ 'pipe', 'pipe', 'pipe' ] 等價(jià)。

          數(shù)組中的每個(gè) fd 都可以是下面的類型(無恥摘錄文檔):

          • 'pipe':在兩個(gè)進(jìn)程之間建立管道。在當(dāng)前進(jìn)程中,該管道以 child.stdio[] 流暴露;而 child.stdin、child.stdoutchild.stderr 分別對應(yīng) child.stdio[0-2]。子進(jìn)程的對應(yīng) fd 會(huì)被重定向到當(dāng)前進(jìn)程的對應(yīng)流中;
          • 'ipc':在兩個(gè)進(jìn)程之間建立 IPC 信道,主子進(jìn)程通過 IPC 互通有無(前提是兩個(gè)進(jìn)程都得是 Node.js 進(jìn)程),不過該類型不應(yīng)用于 std*,而應(yīng)該是數(shù)組中后續(xù)的 fd 中;
          • 'ignore':將 /dev/null 給到對應(yīng)的 fd
          • 'inherit':字面意思是繼承當(dāng)前進(jìn)程,該配置會(huì)將子進(jìn)程的對應(yīng) fd 通過當(dāng)前進(jìn)程的流重定向到當(dāng)前進(jìn)程對應(yīng)的 fd 中,不過只有前三項(xiàng)(stdin、stdoutstderr)會(huì)生效,后續(xù) fd 若配置了 inherit 等同于 ignore;
          • Stream:直接是與子進(jìn)程相關(guān)的 TTY、文件、Socket、管道等可讀或者可寫流對象,該流對象底層的 fd 會(huì)與子進(jìn)程對應(yīng)的 fd 進(jìn)行共享,不過前提是流中得有個(gè)底層的文件描述符,像一個(gè)未打開的文件流對象就還沒有對應(yīng)的描述符;
          • 正整數(shù):與 Stream 類似,對應(yīng)的是一個(gè)文件描述符;
          • null / undefined:保持對應(yīng) fd 的默認(rèn)值,前三個(gè) fd 默認(rèn)為 pipe,之后的為 ignore

          了解了之后,我們就可以做限定了,本文標(biāo)題的意思即 pipe 這類需要消費(fèi)子進(jìn)程 stdio 的操作我們需要真的消費(fèi)才行。

          其實(shí)原因也在文檔中寫明了,我會(huì)在本文的最后再放出來。

          先開始做實(shí)驗(yàn)吧。

          實(shí)驗(yàn)一下

          我們先準(zhǔn)備子進(jìn)程文件:

          child.js

          文件中有兩句 str 聲明,一句為注釋。當(dāng)我們要用短字符串的時(shí)候,就用原代碼;當(dāng)我們要用長字符串的時(shí)候,兩句源碼與注釋互相替換一下。

          短字符串測試

          我們寫如下的主進(jìn)程代碼:

          index.js

          運(yùn)行一下 $ node index.js。一切正常,我們的 'hello' 也被輸出了。沒問題。然后在上面的代碼中加入:

          partial: index.js

          再運(yùn)行一下,似乎沒什么變化。脫褲子放屁。我們再加點(diǎn)料吧:

          index.js

          再運(yùn)行一下,把 '123' 輸出了。一切如我們所料一樣。

          長字符串測試

          接下去,我們要注釋掉子進(jìn)程的短字符串,把長字符串放出來吧。

          首先是「短字符串測試」中的最后一段代碼,即有 chunk => data += chunk.toString() 這段代碼的文件。運(yùn)行一下 $ node index.js 看結(jié)果。

          嚯,輸出了一堆的 '0',就像這樣:

          0000000000

          看著太心煩了,把 data 相關(guān)的代碼去掉吧,stdoutdata 事件監(jiān)聽改回這樣:

          partial: index.js

          然后在 console.log 那里也改回 'hello'。再運(yùn)行一遍,世界清凈了,只剩 hello

          到目前為止,一切看起來都還算正常。

          翻車記錄

          接下去要開始翻車了,我們把 child.stdout.on 這一整句去掉,讓主進(jìn)程代碼恢復(fù)成最初的樣子,順便加點(diǎn)料:

          index.js

          $ node index.js,按下手中的回車鍵執(zhí)行吧:

          死月沙雕

          噢,死月真是個(gè)沙雕呢。」之連環(huán)暴擊。

          我們的程序卡住了。上面的源碼很短,一眼就能看出來是因?yàn)闆]執(zhí)行到 process.exit(0) 才卡住的。沒執(zhí)行到 process.exit() 的原因其實(shí)是因?yàn)闆]有觸發(fā) child.on('exit') 事件,再往上推,則是子進(jìn)程沒有退出。

          不信他沒退出的話,在「死月沙雕」的期間看看進(jìn)程存活狀態(tài)就知道了。

          $ ps aux

          動(dòng)手 GDB 一下

          先不看答案,我們動(dòng)手 GDB 一下看看卡哪了。大家編一個(gè) Node.js 的 Debug 版本也要好久,為了簡化過程,我們用 C 寫一個(gè)最簡單的子進(jìn)程就能做好這個(gè)實(shí)驗(yàn)。

          child.c

          然后編譯一下:

          生成了 a.out,然后改一下 JavaScript 主進(jìn)程源碼的 spawn() 函數(shù):

          partial: index.js

          跑起來之后肯定依舊是沙雕一日游。這個(gè)時(shí)候我們拿到 PID 進(jìn)行 GDB 一下吧。

          gdb

          我們看到是卡在 child.c 的第 4 行 printf 了。它上面的執(zhí)行棧也是一路 printf 卡到底。

          現(xiàn)在我們知道了,當(dāng)我們不處理這些文章開始說的事件時(shí)候,子進(jìn)程有可能會(huì)卡在形如 printf 等往 stdout、stderr 這些 fd 寫的操作上。

          Unix Domain Socket 緩沖區(qū)

          我們回過頭去看看,我們的實(shí)驗(yàn)代碼主子進(jìn)程之間是通過什么來聯(lián)立 stdout 的。根據(jù)最開始的文檔摘錄,噢,原來是 pipe 呢!

          噢,原來是我家的房子呢

          通常情況下,Linux 下的管道緩沖區(qū)為 65536 字節(jié)。然而 Node.js 子進(jìn)程 stdio 的值若為 pipe,則其實(shí)是建立了一個(gè) Unix Domain Socket。

          也就是說,子進(jìn)程的 stdout 是一條與主進(jìn)程之間建立起來的 Unix Domain Socket。其兩端的進(jìn)程均將該管道看做一個(gè)文件,子進(jìn)程負(fù)責(zé)往其中寫內(nèi)容,而主進(jìn)程則從中讀取。

          讓我們把視線放到工地上。

          管道

          管道是有大小的。如果我們堵住管道的出口,那么我們一直往管道里面灌水,最終會(huì)導(dǎo)致水灌不進(jìn)去堵住了。這句話同樣適用于我們上面的代碼。

          也就是說,我們最開始沒有翻車的代碼,因?yàn)檩敵龅膬?nèi)容太少,占不滿管道緩沖區(qū),所以不會(huì)阻塞程序執(zhí)行,最終得以安全退出;而后面翻車則是因?yàn)槲覀冚敵龅膬?nèi)容太多了,導(dǎo)致不一會(huì)兒緩沖區(qū)就滿了,而我們的主進(jìn)程又沒去消費(fèi),所以就翻車了。

          主進(jìn)程停止讀取

          為什么我們 on('data') 了就能消費(fèi),而不加就沒消費(fèi)呢。按理說 Node.js 都讀過來,emit 了事,就能繼續(xù)讀下一趴了。其實(shí)不是的。

          看看 Node.js 的判斷 Readable Stream 是否要讀取新內(nèi)容的邏輯(https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L586-L621)。

          maybeReadMore_()

          前面其它正常的前提我們拋開不講,如流正在讀啊,還能讀到數(shù)據(jù)啊什么的。

          當(dāng) Readable Stream 內(nèi)部的 Buffer 長度沒到水位線(通常是 16384),或者其處于 flowing 狀態(tài)且緩存沒數(shù)據(jù)的時(shí)候,該流會(huì)繼續(xù)從源讀數(shù)據(jù)。

          一個(gè) Readable Stream 最開始的 flowing 狀態(tài)是 null。也就是說在這個(gè)狀態(tài)下,當(dāng)達(dá)到緩存水位線之后,就不會(huì)繼續(xù)讀數(shù)據(jù)了。

          那什么時(shí)候這個(gè)狀態(tài)會(huì)變呢?在這里:https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L868-L897。

          當(dāng)你調(diào)用了 stream.on() 的時(shí)候,它會(huì)判斷你這次調(diào)用所監(jiān)聽的事件。若事件是 'data' 且當(dāng)前的 flowing 狀態(tài)不為 false 的話:

          stream.on('data')

          Readable Stream 就會(huì)執(zhí)行 resume()。在 resume() 中(https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L955-L969):

          stream.resume()

          會(huì)將 flowing 設(shè)置為 true。正如 Node.js 文檔中說的一樣:

          All Readable streams begin in paused mode but can be switched to flowing mode in one of the following ways:

          • Adding a 'data' event handler.
          • Calling the stream.resume() method.
          • Calling the stream.pipe() method to send the data to a Writable.

          即所有的 Readable Stream 一開始都處于暫停狀態(tài),對其添加 data 事件才會(huì)開始切為 flowing 狀態(tài)。而在暫停狀態(tài)下,stdiopipe 流會(huì)先緩存略大于或等于水位線的數(shù)據(jù)。

          在暫停狀態(tài)下,只有你添加了 data 事件處理器才會(huì)開始讀取數(shù)據(jù)并丟給你;而如果你處于 flowing 狀態(tài),只移除消費(fèi)者,那么這些數(shù)據(jù)就會(huì)丟失——因?yàn)榱髌鋵?shí)并沒有暫停。

          文檔上雖說一開始處于暫停狀態(tài)時(shí)我們沒去監(jiān)聽數(shù)據(jù),那么流就不會(huì)產(chǎn)生數(shù)據(jù)。實(shí)際上在內(nèi)部實(shí)現(xiàn)上是產(chǎn)生了數(shù)據(jù),而這部分?jǐn)?shù)據(jù)是被緩存起來了。

          念文檔

          好了,回到最開始。我之前說了“其實(shí)原因也在文檔中寫明了,我會(huì)在本文的最后再放出來”?,F(xiàn)在是時(shí)候了,看看這里:https://nodejs.org/api/child_process.html#child_process_child_process。

          By default, pipes for stdin, stdout, and stderr are established between the parent Node.js process and the spawned child. These pipes have limited (and platform-specific) capacity. If the child process writes to stdout in excess of that limit without the output being captured, the child process will block waiting for the pipe buffer to accept more data. This is identical to the behavior of pipes in the shell. Use the { stdio: 'ignore' } option if the output will not be consumed.

          默認(rèn)情況下,spawn 等會(huì)在 Node.js 進(jìn)程與子進(jìn)程間建立 stdin、stdoutstderr 的管道。管道容量有限(不同平臺(tái)容量不同)。如果子進(jìn)程往 stdout 寫入內(nèi)容,而另一端沒有捕獲導(dǎo)致管道滿了的話,在管道騰出空間前,子進(jìn)程就會(huì)一直阻塞。該行為與 Shell 中的管道一致。如果我們不關(guān)心輸出內(nèi)容的話,請?jiān)O(shè)置 { stdio: 'ignore' }

          看吧,就是這個(gè)理兒。如果我們將其設(shè)為 ignore 的話,其三個(gè) std* 就會(huì)導(dǎo)到 /dev/null 去。

          小結(jié)

          所以標(biāo)題的標(biāo)題黨就是這個(gè)意思。

          你一旦建立了子進(jìn)程,且其 stdout 之類的是一個(gè) pipe,你就必須對它的數(shù)據(jù)負(fù)責(zé)。哪怕你只是監(jiān)聽了這個(gè)事件,里面寫個(gè)空函數(shù),Node.js 也會(huì)認(rèn)為你消費(fèi)了,不然 Node.js 會(huì)把子進(jìn)程的數(shù)據(jù)一直掛載在它 Stream 的緩存中,最后到一個(gè)水位(大于 16384 的時(shí)候)之后就停止讀取子進(jìn)程數(shù)據(jù)了。然后就會(huì)導(dǎo)致子進(jìn)程寫阻塞。

          想跟我更深入探討各種問題嗎?附簡歷來找我吧 kaidi.zkd#alibaba-inc.com。略略略 (′?????)

          》》面試官都在用的題庫,快來看看《

          瀏覽 40
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色一级视频在线播放 | 无码国产精品96久久久久孕妇 | 欧美精品一一色哟哟 | 韩国一区二 | 请立即播放黑人大屌日白人小嫩逼的视频 |