淺談多進程程序的控制和管理

多線程程序、多進程程序是當前單機應用經常使用並行化的手段,線程是能夠直接被CPU調度的執行單元,雖然多進程程序中每一個進程也能夠是多線程的,可是本文主要討論的多進程程序默認是每一個進程都有一個單獨線程的狀況。多線程程序和多進程程序,涉及到的線程間和進程間的通訊、同步原語基本都是相同的,因此二者的開發在必定程度上有着高度的類似性,但同時差別化也十分的明顯,因此高性能程序使用多線程仍是多進程實現經常也是爭論的焦點。php

雖然本身以前開發的程序基本都是基於pthreads和C++ std::thread的多線程程序,可是多進程程序仍是有它相應的用武之地的,好比大名鼎鼎的Nginx中master和worker機制就是採用多進程的方式實現的,因此這裏也對多進程和多線程程序的區別聯繫整理一下,最後順便看看Nginx中master和worker進程的管理和實現機制,在後續開發多進程程序的時候能夠直接借鑑使用。html

1、多線程和多進程程序

Linux中有一句耳熟能詳的話——線程被認爲是輕量級的進程,在現代操做系統的概念中,進程被認爲是資源管理的最小單元,而線程被認爲是程序執行的最小單元,因此多線程和多進程之間的差別基本體如今執行單元之間對資源耦合度的差別。雖然對於用戶空間而言,最爲廣爲使用的pthreads線程庫提供了本身一套線程建立和管理、線程間同步接口,其實在Linux下面創線程和建立進程都是使用clone()系統調用實現的,只是在調用參數(flags)上不一樣,致使建立的執行單元具備不同的資源共享狀況,從而造就了線程和進程實質上的差別。linux

1.1 多線程的特色 multi-threaded

從上面的圖中看出,同一個進程中的多個線程,跟執行狀態相關的資源都是獨立的,好比:運行棧、優先級、程序計數器、信號掩碼等都是獨立的,而打開的文件描述符(包含套接字)、地址空間(除了函數中的自動變量屬於棧管理,還有新提出來的線程局部變量,其它基本都是共享的)都是共享的。這裏還設計到信號處理句柄、信號掩碼等,由於在多線程中信號的問題比較的複雜,後面單獨列出來解釋。nginx

共享相同的地址空間、文件描述符給程序的開發帶來了極大的便利,建立多線程的開銷要小的多,並且在運行中任務切換損失也很小,不少的緩存都維持有效的,還有好比負責套接字listen的線程和工做線程之間能夠方便的傳遞網絡鏈接建立的套接字,生產線程和消費線程能夠方便的用隊列進行數據交換,程序設計也能夠特化出日誌記錄、數據落盤等工做線程各司其職。可是天下沒有免費的午飯,任何的便利都是須要付出代價的,多個執行單元能夠訪問資源意味着共享資源必須獲得保護和同步,這是多線程程序設計不可迴避的問題:xcode

(1). 多個線程能夠安全的訪問只讀的資源,可是哪怕只有一個修改者也是不安全的,額外說一句,咱們說的保護是保護的資源,而不是行爲;緩存

(2). 傳統不少庫函數都不是線程安全的,這些函數當初設計的時候沒有考慮到多線程的問題,因此使用了大量的全局變量和靜態局部變量,這些函數是不可重入的。因此在你調用庫函數、連接別人庫的時候,必定要看看有沒有」_r」後綴的版本;安全

(3). 還要就是以前不斷被提到的內存模型,由於同個進程中的多個線程可能會並行的執行,這時候若是在線程之間有高速度的數據同步需求的時候,必須讓資源的更新可以及時地被別的線程感知到;服務器

(4). 多線程程序正由於線程之間共享的資源太多,因此若是一個線程出現嚴重的問題,其他的線程也會被殺死。遙想當年在TP-LINK的時候,全部的服務功能都以線程的形式被包裹在一個用戶進程中,某個模塊出現問題均可能致使上不了網須要重啓,因此如今看來穩定運行的TP-LINK路由器不得不說是一個奇蹟~網絡

1.2 多進程的特色 multi-process

多進程程序之間保證了資源的高度隔離,只在建立出來的父子進程之間有少許的聯繫,進程組、回話等就不在此討論了。多線程

這個時候須要共享的資源必須顯式共享,雖然操做系統優化機制可讓他們的只讀數據(好比執行代碼)物理上共享,進程間的資源共享或者經過關聯到文件系統的某個路徑或者文件,或者經過全局字符串名字方式,經過以某個進程首先建立資源,其餘進程打開資源的方式共享。因爲歷史緣由,Linux進程間通訊一般包含SYS V和Posix兩套接口,其種類和功能大同小異,可是我的的實際感覺Posix的操做接口要更加的好用一些。

Linux進程間通訊一般用到的方法有:匿名管道、命名管道、信號、消息隊列、共享內存、信號量和套接字,其中匿名管道只用於有親屬關係的父子進程之間的一種單功通訊方式,在fork()建立進程以前建立匿名管道。其中我的用的最多的是命名管道、共享內存和信號量:命名管道因爲返回的文件描述符,能夠十分方便的融合到現有的select/poll/epoll框架下面去;信號量主要用於模擬進程間互斥的行爲;共享內存用於進程間大規模的數據共享。陳碩的一句名言就是「在多進程之間共享內存無異於掩耳盜鈴」,其實多進程間經過共享內存的方式共享數據弊端和限制確實不少:首先共享內存中不能共享指針,而指向共享內存段自己的指針也最好用便宜的方式退化指針;若是共享內存的數據常常會被修改,那更是個災難。固然簡單隻讀數據是能夠的,好比Nginx的緩存也使用了共享內存。

多進程程序的好處,就是消除了進程之間的耦合度後,操做系統的保護機制可讓多個進程更加的獨立可靠,並且分紅多個進程以後管理進程比管理線程方便靈活的多;同時,多進程程序能夠實現進程的特異化管理,好比在Nginx設計中master process是特權進程,能夠讀取配置文件、修改數重要數據等關鍵操做,而worker process是普通權限進程,只負責業務方面的處理,符合系統管理中的最小化權限原則;再有就是多進程程序能夠進行業務的熱更新平滑升級,下面的Nginx算是將這一功能使用的淋漓盡致啊。

可是多進程的程序也有個問題,就是不少共享的資源、同步的手段都是命名全局的,頗有可能進程意外退出後這些資源都得不到回收,補救的辦法只能是重啓操做系統,汗~

1.3 多線程程序和信號

感受信號一直是Linux平臺下開發比較頭疼的問題,尤爲對於多線程狀況下的程序,信號的處理將更加的複雜。

1.3.1 單線程程序中信號的處理方式

Linux中的信號的處理方式能夠是SIG_IGN、SIG_DFL以及本身經過sigaction設置自定義處理函數,進程建立的時候信號都有默認的處理方式,而用戶能夠後續選擇忽略、默認處理方式、自定義處理這些信號(SIGKILL、SIGSTOP兩個信號只能默認處理方式,不能被忽略或者重定義處理),當進程接收到信號的時候就會轉向信號處理歷程去執行。

信號能夠在某些狀況下被系統發送(好比觸發段錯誤),或者被別的進程使用kill發送,或者進程本身調用kill、raise系統調用觸發信號。進程能夠經過signal mask去block某些信號,默認狀況下是沒有信號被block的,此時若是被block的信號發送過來了,將會被設置爲pending的,而後一旦該進程unblock了該信號,pending的信號將會當即被傳遞。

1.3.2 pthreads庫多線程環境對信號處理的方式

pthreads庫多線程中信號處理的方式,和信號的種類、各個線程對信號的mask狀態共同決定的。

Linux中多線程環境下信號的種類能夠分爲同步(Synchronously)信號和異步(Asynchronously)信號:同步信號是針對某個線程的,好比某個線程執行過程當中除以零(SIGFPE)、訪問非法地址(SIGSEGV)、使用了broken的管道(SIGPIPE),這些信號都根某個特定的線程特定的執行上下文有關,還有就是同個進程中線程之間經過pthread_kill顯式發送信號的狀況;異步信號主要是其餘進程向該進程經過kill向這個進程(而非其中的線程)發送信號,並不跟某個特定的線程相關聯的狀況。

pthreads庫中多線程之間共享sigaction結構可是不共享sig_mask結構,這意味全部的線程共享相同的信號處理方式,而不論信號處理方式是誰設置的。進程在最初fork()後建立的第一個線程繼承了其signal mask,而經過pthread_create建立的其餘線程也繼承了這個信號mask,後續能夠經過pthread_sigmask接口控制本線程對某些信號的block或者unblock。

有了上面的知識,信號在多線程下的行爲就能夠被肯定了:

(1). 全部的線程共享相同的sigaction,因此全部進程對某個信號的處理方式是徹底相同的;

(2). 同步信號是針對某個特定線程的,該線程是否接收處理這個信號看其signal mask設置狀況;

(3). 異步信號是針對這個進程的,當這種信號到達的時候,進程會從沒有block這個信號的線程集合中隨機選出一個出來處理這個信號,若是全部的線程都block該信號,那麼這個信號將被pending起來,直到有線程unblock這個信號,就將其發送給那個線程處理;

2、master管理多個worker進程

在Nginx的配置文件中有個條目worker_processes,其用於指定master進程能夠產生幾個worker進程,默認狀況下是CPU執行單元的數目。在Linux下實驗發現,當kill掉worker進程的時候,master進程會自動再次啓動worker進程,可是當kill掉master進程的時候,worker進程仍然活着並向外提供服務,這種方式或許是對於常駐服務最好的處理語義:master進程存在的時候會保證設定數目的工做進程存在,而master進程掛掉的時候worker進程仍然繼續服務,不會存在單點故障致使服務當即中止的狀況。

其基本原理也很簡單,這源於在Linux平臺下,當子進程退出的時候,內核會向父進程發送SIGCHLD信號,父進程能夠捕獲這個信號,並經過wait系統調用搜集子進程退出的相關信息,此後子進程的資源會被相應的釋放掉。所以,父進程能夠經過接收信號的方式異步獲得子進程退出的消息,而且適當安排建立工做者進程。

固然,這僅僅是一個小trick,探究一下,發現Nginx的設計中,尤爲是多進程服務端程序的開發維護中,大有學問能夠借鑑!同時還有一個跟Nginx關係十分密切,估計也是使用相同master-worker方式構建的多進程的構架的,那就是php-fpm。之因此說關係密切,就是由於Apache自己支持php的解析,而Nginx只能經過外掛的方式,而掛件最多見的恰巧就是php-fpm了,經過ps查看,其也像是master-worker的結構,不過沒看代碼尚且不敢判定。

2.1 跟蹤環境的配置

不知道啥時候,本身都快成了代碼控了,GitHub上面一些感興趣的項目代碼都會clone下來並不斷pull跟蹤,nginx就是其中之一啊。調試環境設置很簡單,只是有些點須要額外注意一下

root@srv:~/nginx# apt-get install libpcre3-dev zlib1g-dev root@srv:~/nginx# auto/configure --with-debug root@srv:~/nginx# make 上面configure的時候必定要添加–with-debug參數,這個時候可讓可執行程序支持生成debug的log信息,同時若是是MacOS的系統的話,還須要事先用homebrew安裝gcc,而後添加–with-cc=/usr/local/bin/gcc-5指定使用gcc編譯器(後面有時間說是要折騰一下Clang的,而蘋果xcode默認就是用的這貨),不過MacOS底層用的是kqueue而不是epoll,你應該知道我要說什麼;make編譯以後會在objs目錄下面生成nginx可執行程序

root@srv:~/nginx# mkdir logs root@srv:~/nginx# objs/nginx -p . 經過-p參數,能夠避免使用默認系統路徑的權限問題,以及對現有環境的干擾。此時進程所有轉到後臺執行了,更要命的是IDE的調試環境此處被斷開失連了,因此須要在nginx.c中將系統初始化過程的ngx_daemon()註釋起來,就能夠正常斷點跟蹤了。

到此,Nginx的調試跟蹤環境設置完成,設置conf/nginx.conf中log級別error_log logs/error.log debug;而後經過tail -f logs/error.log全部運行調試日誌一覽無餘。

2.2 多進程服務端程序設計

經過官網Nginx文檔大體瞭解了一下他的構架,看的真是讓人拍案叫絕大快人心,請待我慢慢道來。

2.2.1 多進程下的套接字

傳統上Nginx在啓動開始的時候就bind一個地址進行listen,後續在fork()建立worker process的時候,這些進程是共享這個偵聽套接字的,這個在linux fork()的手冊中明確地被表示出了

The child inherits copies of the parent’s set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent. The child inherits copies of the parent’s set of open message queue descriptors, open directory streams.

因此master process建立出來的全部worker process都是能夠accept()客戶端請求的,當多個進程對同一個socket調用accept()接收鏈接的時候,他們都會把本身放到這個套接字的等待隊列上面去,而後一旦有客戶發起鏈接請求,這個隊列上面等待的進程就會被喚醒,這個過程在以前分析epoll的時候就介紹過了,可是在較早的epoll版本中,上面的喚醒過程會產生驚羣(Thundering Herd)的問題:即便只有一個鏈接請求到來,也會喚醒在這個共享偵聽套接字上全部等待的進程,而全部進程爭搶這個鏈接只有一個能得到鏈接,其餘全部進程都無功而返,因此新版的epoll添加了EPOLLEXCLUSIVE這麼一個新的flag,經過在EPOLL_CTL_ADD的時候使用,保證在事件就緒的時候不會產生驚羣的問題。

Nginx對於共享accept套接字驚羣問題的處理,有三個方法:

(1). accept_mutex = on

當這個選項打開的時候,worker process在其任務循環的時候,會首先經過ngx_trylock_accept_mutex去得到一個進程間的ngx_accept_mutex互斥鎖,而該鎖一般是使用文件鎖來實現的。在持有這個鎖的時候,首先收集底層就緒的事件,同時執行accept的全部回調,而後釋放該鎖,處理通常的非accept事件。

(2). accept_mutex = off

這個設置在較新版本的Nginx已是默認關閉的,主要考慮到的是:一來經過EPOLLEXCLUSIVE、下面的SO_REUSEPORT等新技術能夠避免accept的時候驚羣的問題;另外一方面Nginx採用基於事件的處理方式,worker process只有不多的幾個,而不像Apache的技術Prefork不少的子進程,因此即便發生驚羣對系統形成的影響也極爲有限。

(3). reuseport

在Linux內核3.9的時候,內核Socket支持了SO_REUSEPORT選項,而Nginx在1.9版本中引入了這個選項,這樣每一個worker process均可以同時偵聽同一個IP:Port地址,內核會發現哪些listener可用,從而自動將鏈接請求分配給給定的worker process,消除了Nginx傳統上經過用戶態採用accept_mutex互斥鎖而帶來的性能損耗問題。

上面三種方式的性能對比在官方也給出了 測試結果 。

2.2.2 基於事件的異步模型

異步模型是新一代http服務器Nginx和老牌Apache最大的不一樣之處:

Apache採用的是Prefork技術,服務啓動以後預先啓動必定數目的子進程,當服務器壓力增大的時候不斷增長子進程的數目,而當服務器空閒後自動關閉一些子進程,雖然這種彈性常駐子進程比One Child per Client的模型要進步不少,可是通過這麼久的多進程、協程開發技術的薰陶可知,子進程的增長只在必定範圍內能夠增長服務能力,同時子進程在進程切換、內存等方面會對服務器帶來很大的壓力,若是當鏈接客戶達到C10K的時候其佔用的資源是不可估量的。

Nginx採用的是基於事件驅動的模型來解決C10K問題,因此一般Nginx只須要啓動不多(一般CPU執行單元個數)的worker process就能夠同時服務大量鏈接,以致於愈來愈多的http服務器遷移到Nginx平臺上面。其工做流程主要是:

當master process經過fork()建立出幾個worker process的時候,worker process進程主執行函數爲ngx_worker_process_cycle(),這裏面除了檢查各類狀態標識(好比接受到父進程發送的信號後,設置ngx_terminate、ngx_quit、ngx_reopen等標識)做出特定行爲外,其正事主要是經過ngx_process_events_and_timers處理事件:

此時若是accept_mutex==on,而當ngx_trylock_accept_mutex搶鎖失敗則直接返回,不然就會設置NGX_POST_EVENTS這個標識,表示事件的回調延後執行。由於咱們要把持鎖的臨界區下降,因此在持鎖的過程當中,經過ngx_process_events(實質乃是ngx_epoll_module_ctx.actions)檢查底層偵聽套接字就緒的事件,根據epoll特性能夠快速的收集就緒事件並添加到ngx_posted_accept_events和ngx_posted_events隊列上去,執行ngx_posted_accept_events隊列回調後釋放鎖,最後執行通常的事件回調操做。

若是accept_mutex==off,那麼在ngx_process_events的過程當中,事件的回調將會在蒐集就緒事件的過程當中同步執行。

2.3 Nginx配置文件和二進制程序平滑升級 Nginx中多進程之間將信號運用的活靈活現(Windows平臺下沒用借用信號的方式,而是用其特有的Event事件進行的通訊),使得Nginx能夠在不間斷服務的狀況下進行配置文件,甚至是二進制文件的平滑升級操做,信號的含義能夠參見ngx_config.h,信號處理參見ngx_process.c:ngx_signal_handler,在信號處理文件中其實也只是設置一些狀態變量,而後在進程的時間循環中去執行相應的操做,好比向worker process發送特定信號、啓動worker process等。

2.3.1 Nginx配置文件平滑升級

經過nginx –s reload或者直接kill -SIGHUP向Nginx master process發送信號,當master process接受到SIGHUP信號的時候:

a. 檢查配置文件,而後打開新的listen socket和日誌文件,若是失敗則讓old nginx繼續執行,不然

b. 建立新的worker process,同時向old worker process發送信息,讓他們graceful關閉,old worker process會關閉偵聽套接字,服務已經鏈接的客戶,當全部鏈接客戶服務完了以後退出

2.3.2 Nginx二進制程序平滑升級

將新的二進制文件拷貝覆蓋原二進制執行文件,而後向master process發送SIGUSR2信號,當master process接收到該信號的時候:

a. 將pid文件從新命名爲nginx.pid.oldbin

b. 執行新的可執行文件,按照常規的路徑會產生new master process和new worker process,此時新老進程所有並存,而且所有正常工做——接受客戶端鏈接請求和服務客戶端

c. 向old master process發送SIGWINCH,其將會把本身全部的old master workers關閉,注意此時old master process的偵聽套接字仍然工做的,必要時候仍是會自動產生本身的worker process。調試新版本升級是否正常:若是正常就向old master process發送SIGQUIT,加上以前SIGWINCH工做全部的old process清理完畢;若是不正常,向old master process發送SIGHUP產生worker process,同時向new master process發送SIGTERM信號當即清理全部的new worker process,而後使用SIGKILL殺死new master process

本文完!

參考 GitHub Nginx

THREADS VS. PROCESSES FOR PROGRAM PARALLELIZATION

On Threads, Processes and Co-Processes

POSIX thread (pthread) libraries

Extending Traditional Signals

Pthreads Programming Chapter 5 - Pthreads and UNIX Threads and Signals

Windows服務端程序向Linux平臺移植事項

深入理解Linux進程間通訊(IPC) Inside NGINX: How We Designed for Performance & Scale

The Architecture of Open Source Applications – NGINX

Controlling nginx

Socket Sharding in NGINX Release 1.9.1

Issues In Concurrent Server Design on Linux Systems - Part I

查看原文: taozj.org/2016/11/淺談多…

原文2

相關文章
相關標籤/搜索