搭個 Web 服務器(三)(搭個彩鋼瓦棚多少錢)

      網友投稿 758 2025-03-31

      譯者:StdioA


      -服務器的基本結構及如何處理請求

      -如何并發處理多個請求

      -進程分叉后不關閉重復的套接字會發生什么?

      -僵尸進程

      -如何處理僵尸進程?

      -正確處理 SIGCHLD 信號

      -大功告成

      “只有在創造中才能夠學到更多?!?——皮亞杰

      在本系列的第二部分中,你創造了一個可以處理基本 HTTP GET 請求的、樸素的 WSGI 服務器。當時我問了一個問題:“你該如何讓你的服務器在同一時間處理多個請求呢?”在這篇文章中,你會找到答案。系好安全帶,我們要認真起來,全速前進了!你將會體驗到一段非??焖俚穆贸獭蕚浜媚愕?Linux、Mac OS X(或者其他 _nix 系統),還有你的 Python。本文中所有源代碼均可在?GitHub?[1]?上找到。

      服務器的基本結構及如何處理請求

      首先,我們來回顧一下 Web 服務器的基本結構,以及服務器處理來自客戶端的請求時,所需的必要步驟。你在第一部分及第二部分中創建的輪詢服務器只能夠一次處理一個請求。在處理完當前請求之前,它不能夠接受新的客戶端連接。所有請求為了等待服務都需要排隊,在服務繁忙時,這個隊伍可能會排的很長,一些客戶端可能會感到不開心。

      這是輪詢服務器?webserver3a.py[2]?的代碼:

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3a.py

      為了觀察到你的服務器在同一時間只能處理一個請求的行為,我們對服務器的代碼做一點點修改:在將響應發送至客戶端之后,將程序阻塞 60 秒。這個修改只需要一行代碼,來告訴服務器進程暫停 60 秒鐘。

      這是我們更改后的代碼,包含暫停語句的服務器?webserver3b.py[3]:

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py

      用以下命令啟動服務器:

      $?python?webserver3b.py

      現在,打開一個新的命令行窗口,然后運行?curl?語句。你應該可以立刻看到屏幕上顯示的字符串“Hello, World!”:

      $?curl?http://localhost:8888/hello ?Hello,?World!

      然后,立刻打開第二個命令行窗口,運行相同的curl命令:

      $?curl?http://localhost:8888/hello

      如果你在 60 秒之內完成了以上步驟,你會看到第二條?curl指令不會立刻產生任何輸出,而只是掛在了哪里。同樣,服務器也不會在標準輸出流中輸出新的請求內容。這是這個過程在我的 Mac 電腦上的運行結果(在右下角用黃色框標注出來的窗口中,我們能看到第二個?curl?指令被掛起,正在等待連接被服務器接受):

      當你等待足夠長的時間(60 秒以上)后,你會看到第一個curl?程序完成,而第二個?curl?在屏幕上輸出了“Hello, World!”,然后休眠 60 秒,進而終止。

      這樣運行的原因是因為在服務器在處理完第一個來自curl的請求之后,只有等待 60 秒才能開始處理第二個請求。這個處理請求的過程按順序進行(也可以說,迭代進行),一步一步進行,在我們剛剛給出的例子中,在同一時間內只能處理一個請求。

      現在,我們來簡單討論一下客戶端與服務器的交流過程。為了讓兩個程序在網絡中互相交流,它們必須使用套接字。你應當在本系列的前兩部分中見過它幾次了。但是,套接字是什么?

      套接字(socket)是一個通訊通道端點(endpoint) 的抽象描述,它可以讓你的程序通過文件描述符來與其它程序進行交流。在這篇文章中,我只會單獨討論 Linux 或 Mac OS X 中的 TCP/IP 套接字。這里有一個重點概念需要你去理解:TCP?套接字對(socket pair)

      TCP 連接使用的套接字對是一個由 4 個元素組成的元組,它確定了 TCP 連接的兩端:本地 IP 地址、本地端口、遠端 IP 地址及遠端端口。一個套接字對唯一地確定了網絡中的每一個 TCP 連接。在連接一端的兩個值:一個 IP 地址和一個端口,通常被稱作一個套接字。(引自《UNIX 網絡編程 卷1:套接字聯網 API (第3版)》[4])

      所以,元組?{10.10.10.2:49152, 12.12.12.3:8888}?就是一個能夠在客戶端確定 TCP 連接兩端的套接字對,而元組?{12.12.12.3:8888, 10.10.10.2:49152}?則是在服務端確定 TCP 連接兩端的套接字對。在這個例子中,確定 TCP 服務端的兩個值(IP 地址?12.12.12.3及端口?8888),代表一個套接字;另外兩個值則代表客戶端的套接字。

      一個服務器創建一個套接字并開始建立連接的基本工作流程如下:

      1.服務器創建一個 TCP/IP 套接字。我們可以用這條 Python 語句來創建:

      listen_socket?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)

      2.服務器可能會設定一些套接字選項(這個步驟是可選的,但是你可以看到上面的服務器代碼做了設定,這樣才能夠在重啟服務器時多次復用同一地址):

      listen_socket.setsockopt(socket.SOL_SOCKET,?socket.SO_REUSEADDR,?1)

      3.然后,服務器綁定一個地址。綁定函數 bind 可以將一個本地協議地址賦給套接字。若使用 TCP 協議,調用綁定函數 bind 時,需要指定一個端口號,一個 IP 地址,或兩者兼有,或兩者全無。(引自《UNIX網絡編程 卷1:套接字聯網 API (第3版)》[5])

      listen_socket.bind(SERVER_ADDRESS)

      然后,服務器開啟套接字的監聽模式。

      listen_socket.listen(REQUEST_QUEUE_SIZE)

      監聽函數?listen?只應在服務端調用。它會通知操作系統內核,表明它會接受所有向該套接字發送的入站連接請求。

      以上四步完成后,服務器將循環接收來自客戶端的連接,一次循環處理一條。當有連接可用時,接受請求函數?accept?將會返回一個已連接的客戶端套接字。然后,服務器從這個已連接的客戶端套接字中讀取請求數據,將數據在其標準輸出流中輸出出來,并向客戶端回送一條消息。然后,服務器會關閉這個客戶端連接,并準備接收一個新的客戶端連接。

      這是客戶端使用 TCP/IP 協議與服務器通信的必要步驟:

      下面是一段示例代碼,使用這段代碼,客戶端可以連接你的服務器,發送一個請求,并輸出響應內容:

      import?socket###?創建一個套接字,并連接值服務器sock?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)sock.connect(('localhost',?8888))###?發送一段數據,并接收響應數據sock.sendall(b'test')data?=?sock.recv(1024)print(data.decode())

      在創建套接字后,客戶端需要連接至服務器。我們可以調用連接函數?connect?來完成這個操作:

      sock.connect(('localhost',?8888))

      客戶端只需提供待連接的遠程服務器的 IP 地址(或主機名),及端口號,即可連接至遠端服務器。

      你可能已經注意到了,客戶端不需要調用?bind?及?accept?函數,就可以與服務器建立連接。客戶端不需要調用?bind?函數是因為客戶端不需要關注本地 IP 地址及端口號。操作系統內核中的 TCP/IP 協議棧會在客戶端調用?connect?函數時,自動為套接字分配本地 IP 地址及本地端口號。這個本地端口被稱為臨時端口(ephemeral port),即一個短暫開放的端口。

      服務器中有一些端口被用于承載一些眾所周知的服務,它們被稱作通用(well-known)端口:如 80 端口用于 HTTP 服務,22 端口用于 SSH 服務。打開你的 Python shell,與你在本地運行的服務器建立一個連接,來看看內核給你的客戶端套接字分配了哪個臨時端口(在嘗試這個例子之前,你需要運行服務器程序webserver3a.py?或?webserver3b.py):

      >>>?import?socket>>>?sock?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)>>>?sock.connect(('localhost',?8888))>>>?host,?port?=?sock.getsockname()[:2]>>>?host,?port ('127.0.0.1',?60589)

      在上面的例子中,內核將臨時端口 60589 分配給了你的套接字。

      在我開始回答我在第二部分中提出的問題之前,我還需要快速講解一些概念。你很快就會明白這些概念為什么非常重要。這兩個概念,一個是進程,另外一個是文件描述符。

      什么是進程?進程就是一個程序執行的實體。舉個例子:當你的服務器代碼被執行時,它會被載入內存,而內存中表現此次程序運行的實體就叫做進程。內核記錄了進程的一系列有關信息——比如進程 ID——來追蹤它的運行情況。當你在執行輪詢服務器?webserver3a.py?或?webserver3b.py時,你其實只是啟動了一個進程。

      我們在終端窗口中運行?webserver3b.py:

      $?python?webserver3b.py

      在另一個終端窗口中,我們可以使用 ps 命令獲取該進程的相關信息:

      $?ps?|?grep?webserver3b?|?grep?-v?grep7182?ttys003????0:00.04?python?webserver3b.py

      ps命令顯示,我們剛剛只運行了一個 Python 進程?webserver3b.py。當一個進程被創建時,內核會為其分配一個進程 ID,也就是 PID。在 UNIX 中,所有用戶進程都有一個父進程;當然,這個父進程也有進程 ID,叫做父進程 ID,縮寫為 PPID。假設你默認使用 BASH shell,那當你啟動服務器時,就會啟動一個新的進程,同時被賦予一個 PID,而它的父進程 PID 會被設為 BASH shell 的 PID。

      自己嘗試一下,看看這一切都是如何工作的。重新開啟你的 Python shell,它會創建一個新進程,然后在其中使用系統調用os.getpid()?及os.getppid()來獲取 Python shell 進程的 PID 及其父進程 PID(也就是你的 BASH shell 的 PID)。然后,在另一個終端窗口中運行?ps?命令,然后用?grep?來查找 PPID(父進程 ID,在我的例子中是 3148)。在下面的屏幕截圖中,你可以看到一個我的 Mac OS X 系統中關于進程父子關系的例子,在這個例子中,子進程是我的 Python shell 進程,而父進程是 BASH shell 進程:

      另外一個需要了解的概念,就是文件描述符。什么是文件描述符?文件描述符是一個非負整數,當進程打開一個現有文件、創建新文件或創建一個新的套接字時,內核會將這個數返回給進程。你以前可能聽說過,在 UNIX 中,一切皆是文件。內核會按文件描述符來找到一個進程所打開的文件。當你需要讀取文件或向文件寫入時,我們同樣通過文件描述符來定位這個文件。Python 提供了高層次的操作文件(或套接字)的對象,所以你不需要直接通過文件描述符來定位文件。但是,在高層對象之下,我們就是用它來在 UNIX 中定位文件及套接字,通過這個整數的文件描述符。

      一般情況下,UNIX shell 會將一個進程的標準輸入流(STDIN)的文件描述符設為 0,標準輸出流(STDOUT)設為 1,而標準錯誤打?。⊿TDERR)的文件描述符會被設為 2。

      我之前提到過,即使 Python 提供了高層次的文件對象或類文件對象來供你操作,你仍然可以在對象上使用fileno()方法,來獲取與該文件相關聯的文件描述符?;氐?Python shell 中,我們來看看你該怎么做到這一點:

      >>>?import?sys >>>?sys.stdin',?mode?'r'?at?0x102beb0c0> >>>?sys.stdin.fileno()0>>>?sys.stdout.fileno()1>>>?sys.stderr.fileno()2

      當你在 Python 中操作文件及套接字時,你可能會使用高層次的文件/套接字對象,但是你仍然有可能會直接使用文件描述符。下面有一個例子,來演示如何用文件描述符做參數來進行一次寫入的系統調用:

      >>>?import?sys>>>?import?os>>>?res?=?os.write(sys.stdout.fileno(),?'hello\n') hello

      下面是比較有趣的部分——不過你可能不會為此感到驚訝,因為你已經知道在 Unix 中,一切皆為文件——你的套接字對象同樣有一個相關聯的文件描述符。和剛才操縱文件時一樣,當你在 Python 中創建一個套接字時,你會得到一個對象而不是一個非負整數,但你永遠可以用我之前提到過的?fileno()?方法獲取套接字對象的文件描述符,并可以通過這個文件描述符來直接操縱套接字。

      >>>?import?socket>>>?sock?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)>>>?sock.fileno()3

      我還想再提一件事:不知道你有沒有注意到,在我們的第二個輪詢服務器?webserver3b.py?中,當你的服務器休眠 60 秒的過程中,你仍然可以通過第二個curl?命令連接至服務器。當然curl命令并沒有立刻輸出任何內容而是掛在哪里,但是既然服務器沒有接受連接,那它為什么不立即拒絕掉連接,而讓它還能夠繼續與服務器建立連接呢?這個問題的答案是:當我在調用套接字對象的?listen?方法時,我為該方法提供了一個?BACKLOG參數,在代碼中用?REQUEST_QUEUE_SIZE?常量來表示。BACKLOG參數決定了在內核中為存放即將到來的連接請求所創建的隊列的大小。當服務器?webserver3b.py?在睡眠的時候,你運行的第二個?curl?命令依然能夠連接至服務器,因為內核中用來存放即將接收的連接請求的隊列依然擁有足夠大的可用空間。

      盡管增大?BACKLOG參數并不能神奇地使你的服務器同時處理多個請求,但當你的服務器很繁忙時,將它設置為一個較大的值還是相當重要的。這樣,在你的服務器調用?accept?方法時,不需要再等待一個新的連接建立,而可以立刻直接抓取隊列中的第一個客戶端連接,并不加停頓地立刻處理它。

      歐耶!現在你已經了解了一大塊內容。我們來快速回顧一下我們剛剛講解的知識(當然,如果這些對你來說都是基礎知識的話,那我們就當復習好啦)。

      輪詢服務器

      服務端套接字創建流程(創建套接字,綁定,監聽及接受)

      客戶端連接創建流程(創建套接字,連接)

      套接字對

      套接字

      臨時端口及通用端口

      進程

      進程 ID(PID),父進程 ID(PPID),以及進程父子關系

      文件描述符

      套接字的?listen?方法中,BACKLOG?參數的含義

      如何并發處理多個請求

      現在,我可以開始回答第二部分中的那個問題了:“你該如何讓你的服務器在同一時間處理多個請求呢?”或者換一種說法:“如何編寫一個并發服務器?”

      在 UNIX 系統中編寫一個并發服務器最簡單的方法,就是使用系統調用?fork()。

      下面是全新出爐的并發服務器?webserver3c.py[6]?的代碼,它可以同時處理多個請求(和我們之前的例子webserver3b.py[7]?一樣,每個子進程都會休眠 60 秒):

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3c.py

      在深入研究代碼、討論fork?如何工作之前,先嘗試運行它,自己看一看這個服務器是否真的可以同時處理多個客戶端請求,而不是像輪詢服務器?webserver3a.py?和?webserver3b.py一樣。在命令行中使用如下命令啟動服務器:

      $?python?webserver3c.py

      然后,像我們之前測試輪詢服務器那樣,運行兩個curl命令,來看看這次的效果。現在你可以看到,即使子進程在處理客戶端請求后會休眠 60 秒,但它并不會影響其它客戶端連接,因為他們都是由完全獨立的進程來處理的。你應該看到你的curl?命令立即輸出了“Hello, World!”然后掛起 60 秒。你可以按照你的想法運行盡可能多的?curl命令(好吧,并不能運行特別特別多 ^_^),所有的命令都會立刻輸出來自服務器的響應 “Hello, World!”,并不會出現任何可被察覺到的延遲行為。試試看吧。

      如果你要理解?fork(),那最重要的一點是:你調用了它一次,但是它會返回兩次?—— 一次在父進程中,另一次是在子進程中。當你創建了一個新進程,那么?fork()在子進程中的返回值是 0。如果是在父進程中,那fork()?函數會返回子進程的 PID。

      我依然記得在第一次看到它并嘗試使用fork()?的時候,我是多么的入迷。它在我眼里就像是魔法一樣。這就好像我在讀一段順序執行的代碼,然后“砰!”地一聲,代碼變成了兩份,然后出現了兩個實體,同時并行地運行相同的代碼。講真,那個時候我覺得它真的跟魔法一樣神奇。

      當父進程創建出一個新的子進程時,子進程會復制從父進程中復制一份文件描述符:

      你可能注意到,在上面的代碼中,父進程關閉了客戶端連接:

      else:??###?父進程 ??????client_connection.close()??#?關閉父進程的副本并循環

      不過,既然父進程關閉了這個套接字,那為什么子進程仍然能夠從來自客戶端的套接字中讀取數據呢?答案就在上面的圖片中。內核會使用描述符引用計數器來決定是否要關閉一個套接字。當你的服務器創建一個子進程時,子進程會復制父進程的所有文件描述符,內核中該描述符的引用計數也會增加。如果只有一個父進程及一個子進程,那客戶端套接字的文件描述符引用數應為 2;當父進程關閉客戶端連接的套接字時,內核只會減少它的引用計數,將其變為 1,但這仍然不會使內核關閉該套接字。子進程也關閉了父進程中listen_socket?的復制實體,因為子進程不需要關注新的客戶端連接,而只需要處理已建立的客戶端連接中的請求。

      listen_socket.close()??###?關閉子進程中的復制實體

      我們將會在后文中討論,如果你不關閉那些重復的描述符,會發生什么。

      你可以從你的并發服務器源碼中看到,父進程的主要職責為:接受一個新的客戶端連接,復制出一個子進程來處理這個連接,然后繼續循環來接受另外的客戶端連接,僅此而已。服務器父進程并不會處理客戶端連接——子進程才會做這件事。

      打個岔:當我們說兩個事件并發執行時,我們所要表達的意思是什么?

      當我們說“兩個事件并發執行”時,它通常意味著這兩個事件同時發生。簡單來講,這個定義沒問題,但你應該記住它的嚴格定義:

      如果你不能在代碼中判斷兩個事件的發生順序,那這兩個事件就是并發執行的。(引自《信號系統簡明手冊 (第二版): 并發控制深入淺出及常見錯誤》[8])

      好的,現在你又該回顧一下你剛剛學過的知識點了。

      在 Unix 中,編寫一個并發服務器的最簡單的方式——使用 fork() 系統調用;

      當一個進程分叉(fork)出另一個進程時,它會變成剛剛分叉出的進程的父進程;

      在進行 fork 調用后,父進程和子進程共享相同的文件描述符;

      系統內核通過描述符的引用計數來決定是否要關閉該描述符對應的文件或套接字;

      服務器父進程的主要職責:現在它做的只是從客戶端接受一個新的連接,分叉出子進程來處理這個客戶端連接,然后開始下一輪循環,去接收新的客戶端連接。

      進程分叉后不關閉重復的套接字會發生什么?

      我們來看看,如果我們不在父進程與子進程中關閉重復的套接字描述符會發生什么。下面是剛才的并發服務器代碼的修改版本,這段代碼(webserver3d.py[9]中,服務器不會關閉重復的描述符):

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3d.py

      用以下命令來啟動服務器:

      $?python?webserver3d.py

      用?curl命令連接服務器:

      $?curl?http://localhost:8888/helloHello,?World!

      好,curl?命令輸出了來自并發服務器的響應內容,但程序并沒有退出,而是仍然掛起。到底發生了什么?這個服務器并不會掛起 60 秒:子進程只處理客戶端連接,關閉連接然后退出,但客戶端的?curl命令并沒有終止。

      所以,為什么?curl不終止呢?原因就在于文件描述符的副本。當子進程關閉客戶端連接時,系統內核會減少客戶端套接字的引用計數,將其變為 1。服務器子進程退出了,但客戶端套接字并沒有被內核關閉,因為該套接字的描述符引用計數并沒有變為 0,所以,這就導致了連接終止包(在 TCP/IP 協議中稱作FIN)不會被發送到客戶端,所以客戶端會一直保持連接。這里也會出現另一個問題:如果你的服務器長時間運行,并且不關閉文件描述符的副本,那么可用的文件描述符會被消耗殆盡:

      使用Control-C?關閉服務器?webserver3d.py,然后在?shell?中使用內置命令 ulimit 來查看系統默認為你的服務器進程分配的可用資源數:

      $?ulimit?-a ????core?file?size??????????(blocks,?-c)?0 ????data?seg?size???????????(kbytes,?-d)?unlimited ????scheduling?priority?????????????(-e)?0 ????file?size???????????????(blocks,?-f)?unlimited ????pending?signals?????????????????(-i)?3842 ????max?locked?memory???????(kbytes,?-l)?64 ????max?memory?size?????????(kbytes,?-m)?unlimited ????open?files??????????????????????(-n)?1024 ????pipe?size????????????(512?bytes,?-p)?8 ????POSIX?message?queues?????(bytes,?-q)?819200 ????real-time?priority??????????????(-r)?0 ????stack?size??????????????(kbytes,?-s)?8192 ????cpu?time???????????????(seconds,?-t)?unlimited????max?user?processes??????????????(-u)?3842 ????virtual?memory??????????(kbytes,?-v)?unlimited????file?locks??????????????????????(-x)?unlimited

      你可以從上面的結果看到,在我的 Ubuntu 機器中,系統為我的服務器進程分配的最大可用文件描述符(文件打開)數為 1024。

      現在我們來看一看,如果你的服務器不關閉重復的描述符,它會如何消耗可用的文件描述符。在一個已有的或新建的終端窗口中,將你的服務器進程的最大可用文件描述符設為 256:

      $?ulimit?-n?256

      在你剛剛運行?ulimit -n 256?的終端窗口中運行服務器?webserver3d.py[10]:

      $?python?webserver3d.py

      然后使用下面的客戶端?client3.py[11]來測試你的服務器。

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/client3.py

      在一個新建的終端窗口中,運行client3.py?然后讓它與服務器同步創建 300 個連接:

      $?python?client3.py?--max-clients=300

      過一會,你的服務器進程就該爆了。這是我的環境中出現的異常截圖:

      這個例子很明顯——你的服務器應該關閉描述符副本。

      僵尸進程

      但是,即使你關閉了描述符副本,你依然沒有擺脫險境,因為你的服務器還有一個問題,這個問題在于“僵尸(zombies)”!

      沒錯,這個服務器代碼確實在制造僵尸進程。我們來看看怎么回事。重新運行你的服務器:

      $?python?webserver3d.py

      在另一個終端窗口中運行以下?curl命令:

      $?curl?http://localhost:8888/hello

      現在,運行 ps 環境,來查看正在運行的 Python 進程。下面是我的環境中 ps 的運行結果:

      $?ps?auxw?|?grep?-i?python?|?grep?-v?grep ????vagrant???9099??0.0??1.2??31804??6256?pts/0????S+???16:33???0:00?python?webserver3d.py ????vagrant???9102??0.0??0.0??????0?????0?pts/0????Z+???16:33???0:00?[python]?

      你看到第二行中,pid 為 9102,狀態為?Z+,名字里面有個?的進程了嗎?那就是我們的僵尸進程。這個僵尸進程的問題在于:你無法將它殺掉!

      就算你嘗試使用?kill -9?來殺死僵尸進程,它們仍舊會存活。自己試試看,看看結果。

      這個僵尸到底是什么,為什么我們的服務器會造出它們呢?一個僵尸進程是一個已經結束的進程,但它的父進程并沒有等待(waited)它結束,并且也沒有收到它的終結狀態。如果一個進程在父進程退出之前退出,系統內核會把它變為一個僵尸進程,存儲它的部分信息,以便父進程讀取。內核保存的進程信息通常包括進程 ID、進程終止狀態,以及進程的資源占用情況。OK,所以僵尸進程確實有存在的意義,但如果服務器不管這些僵尸進程,你的系統將會被壅塞。我們來看看這個會如何發生。首先,關閉你運行的服務器;然后,在一個新的終端窗口中,使用?ulimit?命令將最大用戶進程數設為 400(同時,要確保你的最大可用描述符數大于這個數字,我們在這里設為 500):

      搭個 Web 服務器(三)(搭個彩鋼瓦棚多少錢)

      $?ulimit?-u?400$?ulimit?-n?500

      在你剛剛運行ulimit -u 400?命令的終端中,運行服務器?webserver3d.py:

      $?python?webserver3d.py

      在一個新的終端窗口中,運行?client3.py,并且讓它與服務器同時創建 500 個連接:

      $?python?client3.py?--max-clients=500

      然后,過一會,你的服務器進程應該會再次爆了,它會在創建新進程時拋出一個?OSError: 資源暫時不可用的異常。但它并沒有達到系統允許的最大進程數。這是我的環境中輸出的異常信息截圖:

      你可以看到,如果服務器不管僵尸進程,它們會引發問題。接下來我會簡單探討一下僵尸進程問題的解決方案。

      我們來回顧一下你剛剛掌握的知識點:

      如果你不關閉文件描述符副本,客戶端就不會在請求處理完成后終止,因為客戶端連接沒有被關閉;

      如果你不關閉文件描述符副本,長久運行的服務器最終會把可用的文件描述符(最大文件打開數)消耗殆盡;

      當你創建一個新進程,而父進程不等待(wait)子進程,也不在子進程結束后收集它的終止狀態,它會變為一個僵尸進程;

      僵尸通常都會吃東西,在我們的例子中,僵尸進程會吃掉資源。如果你的服務器不管僵尸進程,它最終會消耗掉所有的可用進程(最大用戶進程數);

      你不能殺死(kill)僵尸進程,你需要等待(wait)它。

      如何處理僵尸進程?

      所以,你需要做什么來處理僵尸進程呢?你需要修改你的服務器代碼,來等待(wait)僵尸進程,并收集它們的終止信息。你可以在代碼中使用系統調用?wait?來完成這個任務。不幸的是,這個方法離理想目標還很遠,因為在沒有終止的子進程存在的情況下調用?wait?會導致服務器進程阻塞,這會阻礙你的服務器處理新的客戶端連接請求。那么,我們有其他選擇嗎?嗯,有的,其中一個解決方案需要結合信號處理以及?wait?系統調用。

      這是它的工作流程。當一個子進程退出時,內核會發送?SIGCHLD?信號。父進程可以設置一個信號處理器,它可以異步響應SIGCHLD?信號,并在信號響應函數中等待(wait)子進程收集終止信息,從而阻止了僵尸進程的存在。

      順便說一下,異步事件意味著父進程無法提前知道事件的發生時間。

      修改你的服務器代碼,設置一個?SIGCHLD?信號處理器,在信號處理器中等待(wait)終止的子進程。修改后的代碼如下(webserver3e.py[12]):

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3e.py

      運行服務器:

      $?python?webserver3e.py

      使用你的老朋友——curl命令來向修改后的并發服務器發送一個請求:

      $?curl?http://localhost:8888/hello

      再來看看服務器:

      剛剛發生了什么?accept調用失敗了,錯誤信息為EINTR。

      當子進程退出并觸發?SIGCHLD?事件時,父進程的?accept?調用被阻塞了,系統轉去運行信號處理器,當信號處理函數完成時,accept?系統調用被打斷:

      別擔心,這個問題很好解決。你只需要重新運行?accept?系統調用即可。這是修改后的服務器代碼webserver3f.py[13],它可以解決這個問題:

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3f.py

      運行更新后的服務器?webserver3f.py:

      $?python?webserver3f.py

      用?curl來向更新后的并發服務器發送一個請求:

      $?curl?http://localhost:8888/hello

      看到了嗎?沒有 EINTR 異常出現了。現在檢查一下,確保沒有僵尸進程存活,調用wait?函數的SIGCHLD?信號處理器能夠正常處理被終止的子進程。我們只需使用?ps?命令,然后看看現在沒有處于Z+狀態(或名字包含??)的 Python 進程就好了。很棒!僵尸進程沒有了,我們很安心。

      如果你創建了一個子進程,但是不等待它,它就會變成一個僵尸進程;

      使用?SIGCHLD?信號處理器可以異步地等待子進程終止,并收集其終止狀態;

      當使用事件處理器時,你需要牢記,系統調用可能會被打斷,所以你需要處理這種情況發生時帶來的異常。

      正確處理 SIGCHLD 信號

      好的,一切順利。是不是沒問題了?額,幾乎是。重新嘗試運行?webserver3f.py?但我們這次不會只發送一個請求,而是同步創建 128 個連接:

      $?python?client3.py?--max-clients?128

      現在再次運行ps?命令:

      $?ps?auxw?|?grep?-i?python?|?grep?-v?grep

      看到了嗎?天啊,僵尸進程又出來了!

      這回怎么回事?當你同時運行 128 個客戶端,建立 128 個連接時,服務器的子進程幾乎會在同一時間處理好你的請求,然后退出。這會導致非常多的?SIGCHLD?信號被發送到父進程。問題在于,這些信號不會存儲在隊列中,所以你的服務器進程會錯過很多信號,這也就導致了幾個僵尸進程處于無主狀態:

      這個問題的解決方案依然是設置 SIGCHLD 事件處理器。但我們這次將會用 WNOHANG 參數循環調用waitpid?來替代?wait,以保證所有處于終止狀態的子進程都會被處理。下面是修改后的代碼,webserver3g.py[14]:

      代碼略,請參考鏈接:https://github.com/rspivak/lsbaws/blob/master/part3/webserver3g.py

      運行服務器:

      $?python?webserver3g.py

      使用測試客戶端 client3.py:

      $?python?client3.py?--max-clients?128

      現在來查看一下,確保沒有僵尸進程存在。耶!沒有僵尸的生活真美好 ^_^。

      大功告成

      恭喜!你剛剛經歷了一段很長的旅程,我希望你能夠喜歡它?,F在你擁有了自己的簡易并發服務器,并且這段代碼能夠為你在繼續研究生產級 Web 服務器的路上奠定基礎。

      我將會留一個作業:你需要將第二部分中的 WSGI 服務器升級,將它改造為一個并發服務器。你可以在?這里?[15]找到更改后的代碼。但是,當你實現了自己的版本之后,你才應該來看我的代碼。你已經擁有了實現這個服務器所需的所有信息。所以,快去實現它吧 ^_^。

      然后要做什么呢?喬希·比林斯說過:

      “就像一枚郵票一樣——專注于一件事,不達目的不罷休?!?/p>

      開始學習基本知識?;仡櫮阋呀泴W過的知識。然后一步一步深入。

      “如果你只學會了方法,你將會被這些方法所困。但如果你學會了原理,那你就能發明出新的方法。”——拉爾夫·沃爾多·愛默生

      “有道無術,術尚可求也,有術無道,止于術”——中國古代也有這樣的話,LCTT 譯注

      下面是一份書單,我從這些書中提煉出了這篇文章所需的素材。他們能助你在我剛剛所述的幾個方面中發掘出兼具深度和廣度的知識。我極力推薦你們去搞到這幾本書看看:從你的朋友那里借,在當地的圖書館中閱讀,或者直接在亞馬遜上把它買回來。下面是我的典藏秘籍:

      《UNIX 網絡編程 卷1:套接字聯網 API (第3版)》[16]

      《UNIX 環境高級編程(第3版)》[17]

      《Linux/UNIX 系統編程手冊》[18]

      《TCP/IP 詳解 卷1:協議(第2版)》[19]

      順便,我在撰寫一本名為《搭個 Web 服務器:從頭開始》的書。這本書講解了如何從頭開始編寫一個基本的 Web 服務器,里面包含本文中沒有的更多細節。訂閱原文下方的郵件列表[22],你就可以獲取到這本書的最新進展,以及發布日期。

      via:?https://ruslanspivak.com/lsbaws-part3/

      本文由?LCTT[24]?原創編譯,Linux中國?榮譽推出

      [1]: https://github.com/rspivak/lsbaws/blob/master/part3/

      [2]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3a.py

      [3]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py

      [4]: http://www.epubit.com.cn/book/details/1692

      [5]: http://www.epubit.com.cn/book/details/1692

      [6]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3c.py

      [7]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py

      [8]: http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG

      [9]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3d.py

      [10]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3d.py

      [11]: https://github.com/rspivak/lsbaws/blob/master/part3/client3.py

      [12]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3e.py

      [13]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3f.py

      [14]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3g.py

      [15]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3h.py

      [16]: http://www.epubit.com.cn/book/details/1692

      [17]: http://www.epubit.com.cn/book/details/1625

      [18]: http://www.epubit.com.cn/book/details/1432

      [19]: http://www.epubit.com.cn/book/details/4232

      [20]: http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG

      [21]: http://greenteapress.com/semaphores/

      [22]: https://ruslanspivak.com/lsbaws-part1/

      [23]: https://github.com/rspivak/

      [24]: https://github.com/LCTT/TranslateProject

      本文轉載自異步社區

      原文鏈接:https://www.epubit.com/articleDetails?id=NC7E3EF91BFF00001DECE16C7CFF01130

      Web應用防火墻 WAF

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Excel怎么設置數據出錯警告
      下一篇:WPS插圖的魔術情結
      相關文章
      国产亚洲Av综合人人澡精品| 亚洲色中文字幕无码AV| 亚洲 综合 国产 欧洲 丝袜| 91亚洲性爱在线视频| 亚洲国产精品一区| 国产AV无码专区亚洲AV男同| 亚洲色成人网站WWW永久| 亚洲色欲久久久综合网| 国产亚洲精品免费视频播放| 国产亚洲精品免费视频播放| 亚洲精品亚洲人成在线观看| 国产综合精品久久亚洲| 亚洲人成色77777| 亚洲av无码乱码国产精品| 久久青青成人亚洲精品| 久久精品国产亚洲| 精品亚洲成a人片在线观看| 91亚洲导航深夜福利| 亚洲春黄在线观看| 亚洲国产精品成人久久久| 亚洲综合丁香婷婷六月香| 亚洲日韩一区精品射精| 精品亚洲福利一区二区| 亚洲精品动漫人成3d在线| 中文字幕不卡亚洲| 亚洲国产精品VA在线观看麻豆| 亚洲产国偷V产偷V自拍色戒| 亚洲一区二区三区日本久久九| 自怕偷自怕亚洲精品| 久久久久精品国产亚洲AV无码| 激情亚洲一区国产精品| 亚洲另类无码一区二区三区| 无码天堂亚洲国产AV| 久久精品国产精品亚洲艾草网美妙| 国产精品亚洲mnbav网站 | 亚洲五月六月丁香激情| 亚洲成人一级电影| 亚洲精品无码久久久久APP | 91亚洲精品视频| 亚洲色精品VR一区区三区| 亚洲国产成人手机在线观看|