首先要明白,Nginx 採用的是多進程(單線程) & 多路IO複用模型。使用了 I/O 多路複用技術的 Nginx,就成了」併發事件驅動「的服務器。html
異步非阻塞(AIO)的詳解http://www.ibm.com/developerworks/cn/linux/l-async/linux
注意 worker 進程數,通常會設置成機器 cpu 核數。由於更多的worker 數,只會致使進程相互競爭 cpu,從而帶來沒必要要的上下文切換。ios
使用多進程模式,不只能提升併發率,並且進程之間相互獨立,一個 worker 進程掛了不會影響到其餘 worker 進程。nginx
主進程(master 進程)首先經過 socket() 來建立一個 sock 文件描述符用來監聽,而後fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),以後子進程 accept() 後將建立已鏈接描述符(connected descriptor)),而後經過已鏈接描述符來與客戶端通訊。編程
那麼,因爲全部子進程都繼承了父進程的 sockfd,那麼當鏈接進來時,全部子進程都將收到通知並「爭着」與它創建鏈接,這就叫「驚羣現象」。大量的進程被激活又掛起,只有一個進程能夠accept() 到這個鏈接,這固然會消耗系統資源。數組
Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖。即每一個 worker 進程在執行 accept 以前都須要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖以後,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚羣問題了。accept_mutex 是一個可控選項,咱們能夠顯示地關掉,默認是打開的。服務器
Nginx在啓動後,會有一個master進程和多個worker進程。網絡
主要用來管理worker進程,包含:接收來自外界的信號,向各worker進程發送信號,監控worker進程的運行狀態,當worker進程退出後(異常狀況下),會自動從新啓動新的worker進程。數據結構
master進程充當整個進程組與用戶的交互接口,同時對進程進行監護。它不須要處理網絡事件,不負責業務的執行,只會經過管理worker進程來實現重啓服務、平滑升級、更換日誌文件、配置文件實時生效等功能。併發
咱們要控制nginx,只須要經過kill向master進程發送信號就好了。好比kill -HUP pid,則是告訴nginx,從容地重啓nginx,咱們通常用這個信號來重啓nginx,或從新加載配置,由於是從容地重啓,所以服務是不中斷的。master進程在接收到HUP信號後是怎麼作的呢?首先master進程在接到信號後,會先從新加載配置文件,而後再啓動新的worker進程,並向全部老的worker進程發送信號,告訴他們能夠光榮退休了。新的worker在啓動後,就開始接收新的請求,而老的worker在收到來自master的信號後,就再也不接收新的請求,而且在當前進程中的全部未處理完的請求處理完成後,再退出。固然,直接給master進程發送信號,這是比較老的操做方式,nginx在0.8版本以後,引入了一系列命令行參數,來方便咱們管理。好比,./nginx -s reload,就是來重啓nginx,./nginx -s stop,就是來中止nginx的運行。如何作到的呢?咱們仍是拿reload來講,咱們看到,執行命令時,咱們是啓動一個新的nginx進程,而新的nginx進程在解析到reload參數後,就知道咱們的目的是控制nginx來從新加載配置文件了,它會向master進程發送信號,而後接下來的動做,就和咱們直接向master進程發送信號同樣了。
而基本的網絡事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。worker進程的個數是能夠設置的,通常咱們會設置與機器cpu核數一致,這裏面的緣由與nginx的進程模型以及事件處理模型是分不開的。
worker進程之間是平等的,每一個進程,處理請求的機會也是同樣的。當咱們提供80端口的http服務時,一個鏈接請求過來,每一個進程都有可能處理這個鏈接,怎麼作到的呢?首先,每一個worker進程都是從master進程fork過來,在master進程裏面,先創建好須要listen的socket(listenfd)以後,而後再fork出多個worker進程。全部worker進程的listenfd會在新鏈接到來時變得可讀,爲保證只有一個進程處理該鏈接,全部worker進程在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀事件,在讀事件裏調用accept接受該鏈接。當一個worker進程在accept這個鏈接以後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,這樣一個完整的請求就是這樣的了。咱們能夠看到,一個請求,徹底由worker進程來處理,並且只在一個worker進程中處理。worker進程之間是平等的,每一個進程,處理請求的機會也是同樣的。當咱們提供80端口的http服務時,一個鏈接請求過來,每一個進程都有可能處理這個鏈接,怎麼作到的呢?首先,每一個worker進程都是從master進程fork過來,在master進程裏面,先創建好須要listen的socket(listenfd)以後,而後再fork出多個worker進程。全部worker進程的listenfd會在新鏈接到來時變得可讀,爲保證只有一個進程處理該鏈接,全部worker進程在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀事件,在讀事件裏調用accept接受該鏈接。當一個worker進程在accept這個鏈接以後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,這樣一個完整的請求就是這樣的了。咱們能夠看到,一個請求,徹底由worker進程來處理,並且只在一個worker進程中處理。
當一個 worker 進程在 accept() 這個鏈接以後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,一個完整的請求。一個請求,徹底由 worker 進程來處理,並且只能在一個 worker 進程中處理。
這樣作帶來的好處:
一、節省鎖帶來的開銷。每一個 worker 進程都是獨立的進程,不共享資源,不須要加鎖。同時在編程以及問題查上時,也會方便不少。
二、獨立進程,減小風險。採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,master 進程則很快從新啓動新的 worker 進程。固然,worker 進程的也能發生意外退出。
多進程模型每一個進程/線程只能處理一路IO,那麼 Nginx是如何處理多路IO呢?
若是不使用 IO 多路複用,那麼在一個進程中,同時只能處理一個請求,好比執行 accept(),若是沒有鏈接過來,那麼程序會阻塞在這裏,直到有一個鏈接過來,才能繼續向下執行。
而多路複用,容許咱們只在事件發生時纔將控制返回給程序,而其餘時候內核都掛起進程,隨時待命。
epoll經過在Linux內核中申請一個簡易的文件系統(文件系統通常用什麼數據結構實現?B+樹),其工做流程分爲三部分:
這樣,註冊好事件以後,只要有 fd 上事件發生,epoll_wait() 就能檢測到並返回給用戶,用戶就能」非阻塞「地進行 I/O 了。
epoll() 中內核則維護一個鏈表,epoll_wait 直接檢查鏈表是否是空就知道是否有文件描述符準備好了。(epoll 與 select 相比最大的優勢是不會隨着 sockfd 數目增加而下降效率,使用 select() 時,內核採用輪訓的方法來查看是否有fd 準備好,其中的保存 sockfd 的是相似數組的數據結構 fd_set,key 爲 fd,value 爲 0 或者 1。)
能達到這種效果,是由於在內核實現中 epoll 是根據每一個 sockfd 上面的與設備驅動程序創建起來的回調函數實現的。那麼,某個 sockfd 上的事件發生時,與它對應的回調函數就會被調用,來把這個 sockfd 加入鏈表,其餘處於「空閒的」狀態的則不會。在這點上,epoll 實現了一個」僞」AIO。可是若是絕大部分的 I/O 都是「活躍的」,每一個 socket 使用率很高的話,epoll效率不必定比 select 高(多是要維護隊列複雜)。
能夠看出,由於一個進程裏只有一個線程,因此一個進程同時只能作一件事,可是能夠經過不斷地切換來「同時」處理多個請求。
例子:Nginx 會註冊一個事件:「若是來自一個新客戶端的鏈接請求到來了,再通知我」,此後只有鏈接請求到來,服務器纔會執行 accept() 來接收請求。又好比向上遊服務器(好比 PHP-FPM)轉發請求,並等待請求返回時,這個處理的 worker 不會在這阻塞,它會在發送完請求後,註冊一個事件:「若是緩衝區接收到數據了,告訴我一聲,我再將它讀進來」,因而進程就空閒下來等待事件發生。
這樣,基於 多進程+epoll, Nginx 便能實現高併發。
使用 epoll 處理事件的一個框架,代碼轉自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html
1 for( ; ; ) 2 { 3 nfds = epoll_wait(epfd,events,20,500); 4 for(i=0;i<nfds;++i) 5 { 6 if(events[i].data.fd==listenfd) //有新的鏈接 7 { 8 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個鏈接 9 ev.data.fd=connfd; 10 ev.events=EPOLLIN|EPOLLET; 11 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中 12 } 13 else if( events[i].events&EPOLLIN ) //接收到數據,讀socket 14 { 15 n = read(sockfd, line, MAXLINE)) < 0 //讀 16 ev.data.ptr = md; //md爲自定義類型,添加數據 17 ev.events=EPOLLOUT|EPOLLET; 18 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓 19 } 20 else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket 21 { 22 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取數據 23 sockfd = md->fd; 24 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發送數據 25 ev.data.fd=sockfd; 26 ev.events=EPOLLIN|EPOLLET; 27 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據 28 } 29 else 30 { 31 //其餘的處理 32 } 33 } 34 }
下面給出一個完整的服務器端例子:
1 #include <iostream> 2 #include <sys/socket.h> 3 #include <sys/epoll.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <fcntl.h> 7 #include <unistd.h> 8 #include <stdio.h> 9 #include <errno.h> 10 11 using namespace std; 12 13 #define MAXLINE 5 14 #define OPEN_MAX 100 15 #define LISTENQ 20 16 #define SERV_PORT 5000 17 #define INFTIM 1000 18 19 void setnonblocking(int sock) 20 { 21 int opts; 22 opts=fcntl(sock,F_GETFL); 23 if(opts<0) 24 { 25 perror("fcntl(sock,GETFL)"); 26 exit(1); 27 } 28 opts = opts|O_NONBLOCK; 29 if(fcntl(sock,F_SETFL,opts)<0) 30 { 31 perror("fcntl(sock,SETFL,opts)"); 32 exit(1); 33 } 34 } 35 36 int main(int argc, char* argv[]) 37 { 38 int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber; 39 ssize_t n; 40 char line[MAXLINE]; 41 socklen_t clilen; 42 43 44 if ( 2 == argc ) 45 { 46 if( (portnumber = atoi(argv[1])) < 0 ) 47 { 48 fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]); 49 return 1; 50 } 51 } 52 else 53 { 54 fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]); 55 return 1; 56 } 57 58 59 60 //聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件 61 62 struct epoll_event ev,events[20]; 63 //生成用於處理accept的epoll專用的文件描述符 64 65 epfd=epoll_create(256); 66 struct sockaddr_in clientaddr; 67 struct sockaddr_in serveraddr; 68 listenfd = socket(AF_INET, SOCK_STREAM, 0); 69 //把socket設置爲非阻塞方式 70 71 //setnonblocking(listenfd); 72 73 //設置與要處理的事件相關的文件描述符 74 75 ev.data.fd=listenfd; 76 //設置要處理的事件類型 77 78 ev.events=EPOLLIN|EPOLLET; 79 //ev.events=EPOLLIN; 80 81 //註冊epoll事件 82 83 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 84 bzero(&serveraddr, sizeof(serveraddr)); 85 serveraddr.sin_family = AF_INET; 86 char *local_addr="127.0.0.1"; 87 inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber); 88 89 serveraddr.sin_port=htons(portnumber); 90 bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr)); 91 listen(listenfd, LISTENQ); 92 maxi = 0; 93 for ( ; ; ) { 94 //等待epoll事件的發生 95 96 nfds=epoll_wait(epfd,events,20,500); 97 //處理所發生的全部事件 98 99 for(i=0;i<nfds;++i) 100 { 101 if(events[i].data.fd==listenfd)//若是新監測到一個SOCKET用戶鏈接到了綁定的SOCKET端口,創建新的鏈接。 102 103 { 104 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); 105 if(connfd<0){ 106 perror("connfd<0"); 107 exit(1); 108 } 109 //setnonblocking(connfd); 110 111 char *str = inet_ntoa(clientaddr.sin_addr); 112 cout << "accapt a connection from " << str << endl; 113 //設置用於讀操做的文件描述符 114 115 ev.data.fd=connfd; 116 //設置用於注測的讀操做事件 117 118 ev.events=EPOLLIN|EPOLLET; 119 //ev.events=EPOLLIN; 120 121 //註冊ev 122 123 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); 124 } 125 else if(events[i].events&EPOLLIN)//若是是已經鏈接的用戶,而且收到數據,那麼進行讀入。 126 127 { 128 cout << "EPOLLIN" << endl; 129 if ( (sockfd = events[i].data.fd) < 0) 130 continue; 131 if ( (n = read(sockfd, line, MAXLINE)) < 0) { 132 if (errno == ECONNRESET) { 133 close(sockfd); 134 events[i].data.fd = -1; 135 } else 136 std::cout<<"readline error"<<std::endl; 137 } else if (n == 0) { 138 close(sockfd); 139 events[i].data.fd = -1; 140 } 141 line[n] = '/0'; 142 cout << "read " << line << endl; 143 //設置用於寫操做的文件描述符 144 145 ev.data.fd=sockfd; 146 //設置用於注測的寫操做事件 147 148 ev.events=EPOLLOUT|EPOLLET; 149 //修改sockfd上要處理的事件爲EPOLLOUT 150 151 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 152 153 } 154 else if(events[i].events&EPOLLOUT) // 若是有數據發送 155 156 { 157 sockfd = events[i].data.fd; 158 write(sockfd, line, n); 159 //設置用於讀操做的文件描述符 160 161 ev.data.fd=sockfd; 162 //設置用於注測的讀操做事件 163 164 ev.events=EPOLLIN|EPOLLET; 165 //修改sockfd上要處理的事件爲EPOLIN 166 167 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 168 } 169 } 170 } 171 return 0; 172 }
事件驅動適合於I/O密集型服務,多進程或線程適合於CPU密集型服務:
一、Nginx 更主要是做爲反向代理,而非Web服務器使用。其模式是事件驅動。
二、事件驅動服務器,最適合作的就是這種 I/O 密集型工做,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉做用,純粹是 I/O 操做,自身並不涉及到複雜計算。由於進程在一個地方進行計算時,那麼這個進程就不能處理其餘事件了。
三、Nginx 只須要少許進程配合事件驅動,幾個進程跑 libevent,不像 Apache 多進程模型那樣動輒數百的進程數。
五、Nginx 處理靜態文件效果也很好,那是由於讀寫文件和網絡通訊其實都是 I/O操做,處理過程同樣。