系統調用java
進程有兩種運行模式:用戶態和內核態。進程一般在用戶態,這時可使用CPU和內存,而當進程須要對硬件外設進行操做的時候(如讀取磁盤文件、發送網絡數據),就必須切換到內核態,當在內核態的任務完成後,進程又切回到用戶態。數據庫
因爲系統調用涉及進程從用戶態到內核態的切換,致使必定的內存空間交換,這也是必定程度上的上下文切換,因此係統調用的開銷一般是比較昂貴的。數組
減小沒必要要的系統調用,也是Web服務器性能優化的一個方面。瀏覽器
咱們使用 strace 來跟蹤 Nginx 的一個子進程,得到某次請求處理的一系列系統調用,以下所示:緩存
內存分配
性能優化
Apache在運行時的內存使用量是很是驚人的,這主要歸咎於它的多進程模型,該模型使得Apache在運行開始便一次性申請大片的內存做爲內存池。而Nginx的內存分配策略,它使用多線程來處理請求,這使得多線程之間能夠共享內存資源,從而令它的內存整體使用大大減小,Nginx維持10000個非活躍HTTP持久鏈接只須要2.5MB內存。服務器
持久鏈接網絡
持久鏈接(Keep-Alive)也稱爲長鏈接。HTTP/1.1對長鏈接有了完整的定義,HTTP請求數據投中包含關於長鏈接的生命:數據結構
Connection : Keep-Alive
長鏈接的有效使用能夠減小大量從新創建鏈接的開銷,有效的加速性能。對於Apache這樣的多進程模型來講,若是長鏈接超時時間過長,那麼即使是瀏覽器沒有任何請求,而Apache仍然維持着鏈接的子進程,一旦併發用戶數較多,那麼Apache將維持着大量空閒進程,嚴重影響了服務器性能。多線程
I/O模型
有人說,比特天生就是用來別複製的,數據的生命意義便在於輸入輸出。
事實上,如何讓高速的CPU和慢速的I/O設備更好的協調工做,這是從現代計算機誕生到如今一直探索的話題。
PIO與DMA
很早之前,磁盤和內存之間的數據傳輸是須要CPU控制的,也就是說如何讀取磁盤文件到內存,數據要通過CPU存儲轉發,這種方式稱爲PIO。
後來,DMA(直接內存訪問,Direct Memory Access)取代了PIO,它能夠不通過CPU而直接進行磁盤和內存的數據交換。在DMA模式下,CPU只須要向DMA下達指令,由DMA來處理數據的傳送便可,DMA經過系統總線來傳輸數據,傳送完畢通知CPU,這樣下降了CPU佔有率。
同步阻塞I/O
阻塞是指當前發起I/O操做的進程被阻塞,而不是CPU被阻塞。
舉個例子,好比你去逛街,餓了,你看到小吃城,就在一家麪館買了一碗麪,交了錢,可麪條作起來須要時間,你知不知道何時能夠作好,只好坐在那裏等,等麪條作好吃完再繼續逛街。—— 這裏吃麪條即是I/O操做。
同步非阻塞I/O
在同步阻塞I/O中,進程實際上等待的時間包括兩部分,一個是等待數據的就緒,另外一個是等待數據的複製(copy data from kenrel to user)。
同步非阻塞I/O的調用不會等待數據的就緒,若是數據不可讀或者不可寫,它會馬上告訴進程。
回到買面的故事,假如你不甘心等麪條作好就想去逛街,可又擔憂麪條作好了沒有及時領取,因此你逛一會便跑回去看看麪條是否作好,往返了不少次,最後雖然即便吃上了麪條,可是卻累得氣喘吁吁。
多路I/O就緒通知
多路I/O就緒通知的出現,提供了對大量文件描述符就緒檢查的高性能方案,它容許進程經過一種方法來同時監視全部文件描述符,並能夠快速得到所喲就緒的文件描述符,而後只針對這些文件描述符進行數據訪問。
回到買面的故事,加入你不止買了一份面,還在其餘小吃店買了餃子、粥、餡餅等,這些東西都須要時間來製做。在同步非阻塞I/O模型中,你要輪流不停地去各個小吃店詢問進度。如今引入多路I/O就緒通知後,小吃城在大廳裏安裝了一塊電子屏幕,之後全部小吃店的食物作好後,都會顯示在屏幕上,這樣你只須要間隔性地看看大屏幕就能夠了也許你還能夠同時逛逛附近的商店。
須要注意的是,I/O就緒通知只是幫助咱們快速獲取就緒的文件描述符,當得知就緒後,就訪問數據自己而言,讓然須要選擇阻塞或非阻塞的方式,通常我麼你選擇非阻塞方式。
多路I/O就緒有不少不一樣的實現:
select
select 最先於1983年出如今4.3BSD中,它經過一個select()系統調用來監視包含多個文件描述符的數組,當select()放回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程能夠得到這些文件描述符從而進行後續的讀寫操做。
select目前在全部平臺上都支持,但select的一個缺點在於單個進程可以監視文件描述符數量存在最大限制,在Linux上通常爲1024,不過能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制。
另外,select()所維護大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增加。同時,因爲網絡響應時間的延遲使得大量TCP鏈接處於非活躍狀態,可是調用select()會對全部socket進行一次線性掃描,因此這也會浪費必定的開銷。
poll
poll在1986年誕生於System V Release3(UNIX),它和select本質上沒有多大差別,除了沒有監視文件數量的限制,select 缺點一樣適用於 poll。
另外,select()和poll()將就緒的文件描述符告訴進程後,若是進程沒有對其進行I/O操做,那麼下次調用的時候將再次報告這些文件描述符,因此通常不會丟失就緒通知的消息,這種方式稱爲水平觸發(Level Triggered)。
SIGIO
Linxu2.4提供SIGIO,它經過實時信號(Real Time Signal)來實現select/poll的通知方式,可是它們的不一樣在於,select/poll告訴咱們哪些文件描述符是就緒的,一直到咱們讀寫以前,每次select/poll都會告訴咱們;而SIGIO則是告訴咱們哪些文件描述符剛剛變爲就緒狀態,它只說一遍,若是咱們沒有采起行動,那麼它就不會再告訴咱們,這種方式稱爲邊緣觸發(Edge Triggered)。
/dev/poll
Sun在Solaris中提供了新的實現,它使用虛擬的/dev/poll設備,你能夠將要監視的文件描述符數組寫入這個設備,而後經過ioctl()來等待時間通知,當ioctl()放回就緒的文件描述符後,你能夠從/dev/poll中讀取全部就緒的文件描述符數組,這點相似於SIGIO。
在Linux下有不少方法能夠實現相似/dev/poll的設備,可是都沒有 提供直接的內核支持,這些方法在服務器負載較大時性能不穩定。
/dev/epoll
隨後,名爲/dev/epoll的設備以不定的形式出如今Linux2.4上,它提供了相似/dev/poll的功能,並且增長了內存映射(mmap)技術,在必定程度上提升了性能。
可是,/dev/epoll仍然只是一個補丁,Linux2.4並無將它的實現加入內核。
epoll
直到Linux2.6纔出現了有內核直接支持的實現方法,那就是epoll,它被公認爲Linxu2.6下性能最好的多路I/O就緒通知方法。
epoll能夠同時支持水平觸發和邊緣出發,在默認狀況下,epoll採用水平觸發,若是要使用邊緣出發,須要在事件註冊時增長EPOLLET選項。
在Nginx的epoll模型代買(src/event/modules/ngx_epoll_module.c)中,能夠看到它採用了邊緣觸發:
ee.events = EPOLLIN | EPOLLOUT | EPOLLET;
另一個本質的改進在於epoll採用基於事件的就緒通知方法。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先通知epoll_ctl()來註冊每個文件描述符,一旦某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。
回到買面的故事,雖然有了電子屏幕,可是顯示的內容是全部食品的狀態,包括正在製做和已經作好的,這顯然給你形成閱讀上的麻煩,就好像select/poll每次返回全部監視的文件描述符同樣,若是可以只顯示作好的食品,隨後小吃城進行了改進,就像/dev/poll同樣只告知就緒的文件描述符。在顯示作好的食品時,若是隻顯示一次,而無論你有沒有看到,這就至關於邊緣出發,而若是在你領取以前,每次都顯示,就至關於水平觸發。
但儘管如此,一旦你走遠了,還得回到小吃城去看電子屏幕,能不能讓你更加輕鬆地得到通知呢?小吃城採起了手機短信通知的方式,你只須要到小吃城管理處註冊後, 即可以在餐點就緒時及時收到短信通知,這相似於epoll的事件機制。
內存映射
Linux內核提供一種訪問磁盤文件的特殊方式,它能夠將內存中某塊地址空間和指定的磁盤文件相關聯,從而把這塊內存的訪問轉換爲對磁盤文件的訪問,這種技術稱爲內存映射(Memory Mapping)。
使用內存映射能夠提升磁盤I/O性能,它無需使用read()或write()等系統調用來訪問文件,而是經過mmap()系統調用來創建內存和磁盤文件的關聯,而後想訪問內存同樣訪問磁盤。
直接I/O
在Linux2.6中,內存映射和直接訪問磁盤文件沒有本質上差別,由於數據從進程用戶態內存空間到磁盤都要通過兩次複製,即"磁盤-->內核緩衝區"和"內核緩衝區-->用戶態內存空間"。
引入內核緩衝區的目的在於提升磁盤文件的訪問性能,由於當進程須要寫磁盤文件時,實際上只是到了內核緩衝區便告訴進程已經成功。然而,對於一些複雜應用,如數據庫服務器,它們爲了充分提升性能,但願繞過內核緩衝區,由本身在用戶態空間實現並管理I/O緩衝區。
Linxu提供了對這種需求的支持,即在open()系統調用中增長了參數選項O_DIRECT,用它打開的文件即可以繞過內核緩衝區的直接訪問,這樣便避免了CPU和內存的多餘開銷。
在MySQL中,對於Innodb存儲引擎,其自身能夠進行數據和索引的緩存管理,因此對於內核緩衝區的依賴不是那麼重要。
sendfile
在向Web服務器請求靜態文件的過程當中,磁盤文件的數據要先通過內核緩衝區,而後到用戶態內存空間,由於是不須要處理的靜態數據,因此它們又被送到網卡對應的內核緩衝區,接着在被送入網卡進行發送。
數據從內核出去,又回到內核,沒有任何變化。在Linux2.4的內核中,引入了一個稱爲khttpd的內核級Web服務器程序,它只處理靜態文件的請求。引入的目的便在於內核但願請求的處理儘可能在內核完成,減小內核態的切換以及用戶態數據複製的開銷。
Linux經過系統調用將這種機制提供給開發者,那就是sendfile()系統調用。它能夠將磁盤文件的特定部分直接傳送到表明客戶端的socket描述符。
異步I/O
阻塞和非阻塞是指當進程訪問的數據若是還沒有就緒,進程是否須要等待。
同步和異步是指訪問數據的機制,同步指請求並等待I/O操做完畢方式,當數據就緒後在讀寫的時候必須阻塞;異步是指請求數據後即可以繼續處理其餘任務,隨後等待I/O操做完畢的通知,這使進程在數據讀寫時不發生阻塞。
參考文章:高性能 IO 模型淺析
—————————— 本文同步發佈於 ZHANGSR 個人我的博客 ——————————