高性能网络编程技术
http://blog.csdn.net/heiyeshuwu/article/details/40508683
高性能網絡編程技術
作者:jmz (360電商技術組)
如何使網絡服務器能夠處理數以萬計的客戶端連接,這個問題被稱為C10K?Problem。在很多系統中,網絡框架的性能直接決定了系統的整體性能,因此研究解決高性能網絡編程框架問題具有十分重要的意義。
1.?網絡編程模型
在C10K?Problem中,給出了一些常見的解決大量并發連接的方案和模型,在此根據自己理解去除了一些不實際的方案,并做了一些整理。
1.1、PPC/TPC模型
典型的Apache模型(Process?Per?Connection,簡稱PPC),TPC(Thread?Per?Connection)模型,這兩種模型思想類似,就是讓每一個到來的連接都一邊自己做事直到完成。只是PPC是為每個連接開了一個進程,而TPC開了一個線程。可是當連接多了之后,如此多的進程/線程切換需要大量的開銷;這類模型能接受的最大連接數都不會高,一般在幾百個左右。
1.2、異步網絡編程模型
異步網絡編程模型都依賴于I/O多路復用模式。一般地,I/O多路復用機制都依賴于一個事件多路分離器(Event?Demultiplexer)。分離器對象可將來自事件源的I/O事件分離出來,并分發到對應的read/write事件處理器(Event?Handler)。開發人員預先注冊需要處理的事件及其事件處理器(或回調函數);事件分離器負責將請求事件傳遞給事件處理器。兩個與事件分離器有關的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用異步IO。
在Reactor中,事件分離器負責等待文件描述符或socket為讀寫操作準備就緒,然后將就緒事件傳遞給對應的處理器,最后由處理器負責完成實際的讀寫工作。
而在Proactor模式中,處理器--或者兼任處理器的事件分離器,只負責發起異步讀寫操作。IO操作本身由操作系統來完成。傳遞給操作系統的參數需要包括用戶定義的數據緩沖區地址和數據大小,操作系統才能從中得到寫出操作所需數據,或寫入從socket讀到的數據。事件分離器捕獲IO操作完成事件,然后將事件傳遞給對應處理器。
l?在Reactor中實現讀:
-?注冊讀就緒事件和相應的事件處理器
-?事件分離器等待事件
-?事件到來,激活分離器,分離器調用事件對應的處理器
-?事件處理器完成實際的讀操作,處理讀到的數據,注冊新事件,然后返還控制權
l?在Proactor中實現讀:
-?處理器發起異步讀操作(注意:操作系統必須支持異步IO)。在這種情況下,處理器無視IO就緒事件,它關注的是完成事件。
-?事件分離器等待操作完成事件
-?在分離器等待過程中,操作系統利用并行的內核線程執行實際的讀操作,并將結果數據存入用戶自定義緩沖區,最后通知事件分離器讀操作完成。
-?事件分離器呼喚處理器。
-?事件處理器處理用戶自定義緩沖區中的數據,然后啟動一個新的異步操作,并將控制權返回事件分離器。
可以看出,兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個模塊,這個IO操作可以進行或已經完成)。在結構上,兩者也有相同點:demultiplexor負責提交IO操作(異步)、查詢設備是否可操作(同步),然后當條件滿足時,就回調handler;不同點在于,異步情況下(Proactor),當回調handler時,表示IO操作已經完成;同步情況下(Reactor),回調handler時,表示IO設備可以進行某個操作(can?read?or?can?write)。
1.2.1?Reactor模式框架
使用Proactor模式需要操作系統支持異步接口,因此在日常中比較常見的是Reactor模式的系統調用接口。使用Reactor模型,必備的幾個組件:事件源、Reactor框架、多路復用機制和事件處理程序,先來看看Reactor模型的整體框架,接下來再對每個組件做逐一說明。
?
l?事件源
Linux上是文件描述符,Windows上就是Socket或者Handle了,這里統一稱為“句柄集”;程序在指定的句柄上注冊關心的事件,比如I/O事件。
l?event?demultiplexer——事件多路分發機制
??由操作系統提供的I/O多路復用機制,比如select和epoll。
??程序首先將其關心的句柄(事件源)及其事件注冊到event?demultiplexer上;
??當有事件到達時,event?demultiplexer會發出通知“在已經注冊的句柄集中,一個或多個句柄的事件已經就緒”;
??程序收到通知后,就可以在非阻塞的情況下對事件進行處理了。
l?Reactor——反應器
Reactor,是事件管理的接口,內部使用event?demultiplexer注冊、注銷事件;并運行事件循環,當有事件進入“就緒”狀態時,調用注冊事件的回調函數處理事件。
一個典型的Reactor聲明方式
class?Reactor?{??
public:??
????int?register_handler(Event_Handler?*pHandler,?int?event);??
????int?remove_handler(Event_Handler?*pHandler,?int?event);??
????void?handle_events(timeval?*ptv);??
????//?...??
};??
l?Event?Handler——事件處理程序
事件處理程序提供了一組接口,每個接口對應了一種類型的事件,供Reactor在相應的事件發生時調用,執行相應的事件處理。通常它會綁定一個有效的句柄。
下面是兩種典型的Event?Handler類聲明方式,二者互有優缺點。
class?Event_Handler?{??
public:??
????virtual?void?handle_read()?=?0;??
????virtual?void?handle_write()?=?0;??
????virtual?void?handle_timeout()?=?0;??
????virtual?void?handle_close()?=?0;??
????virtual?HANDLE?get_handle()?=?0;??
????//?...??
};??
class?Event_Handler?{??
public:??
????//?events?maybe?read/write/timeout/close?.etc??
????virtual?void?handle_events(int?events)?=?0;??
????virtual?HANDLE?get_handle()?=?0;??
????//?...
};??
1.2.2?Reactor事件處理流程
前面說過Reactor將事件流“逆置”了,使用Reactor模式后,事件控制流可以參見下面的序列圖。
?
1.3?Select,poll和epoll
在Linux環境中,比較常見的I/O多路復用機制就是Select,poll和epoll,下面對這三種機制進行分析和比較,并對epoll的使用進行介紹。
1.3.1?select模型
1.?最大并發數限制,因為一個進程所打開的FD(文件描述符)是有限制的,由FD_SETSIZE設置,默認值是1024/2048,因此Select模型的最大并發數就被相應限制了。
2.?效率問題,select每次調用都會線性掃描全部的FD集合,這樣效率就會呈現線性下降,把FD_SETSIZE改大的后果就是所有FD處理都慢慢來
3.?內核/用戶空間?內存拷貝問題,如何讓內核把FD消息通知給用戶空間呢?在這個問題上select采取了內存拷貝方法。
int?res?=?select(maxfd+1,?&readfds,?NULL,?NULL,?120);
if?(res?>?0)?{
????for?(int?i?=?0;?i?<?MAX_CONNECTION;?i++)?{
????????if?(FD_ISSET(allConnection[i],&readfds))?{
????????????handleEvent(allConnection[i]);
????????}
????}
}
1.3.2?poll模型
基本上效率和select是相同的,select缺點的2和3都沒有改掉。
1.3.3?epoll模型
1.?Epoll沒有最大并發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大于2048,?一般來說這個數目和系統內存關系很大,具體數目可以cat?/proc/sys/fs/file-max察看。
2.?效率提升,Epoll最大的優點就在于它只管你“活躍”的連接,而跟連接總數無關,應用程序就能直接定位到事件,而不必遍歷整個FD集合,因此在實際的網絡環境中,Epoll的效率就會遠遠高于select和poll。
int?res?=?epoll_wait(epfd,?events,?20,?120);
for(int?i?=?0;?i?<?res;?i++)?{
????handleEvent(events[n]);
}
3.?內存拷貝,Epoll在這點上使用了“共享內存”,這個內存拷貝也省略了。?
1.3.4?使用epoll
Epoll的接口很簡單,只有三個函數,十分易用。?
int?epoll_create(int?size);
生成一個epoll專用的文件描述符,其實是申請一個內核空間,用來存放你想關注的socket?fd上是否發生以及發生了什么事件。size就是你在這個Epoll?fd上能關注的最大socket?fd數,大小自定,只要內存足夠。
int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);
控制某個Epoll文件描述符上的事件:注冊、修改、刪除。其中參數epfd是epoll_create()創建Epoll專用的文件描述符。相對于select模型中的FD_SET和FD_CLR宏。
int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout);
等待I/O事件的發生;參數說明:
??epfd:由epoll_create()生成的Epoll專用的文件描述符;
??epoll_event:用于回傳代處理事件的數組;
??maxevents:每次能處理的事件數;
??timeout:等待I/O事件發生的超時值;
??返回發生事件數。
上面講到了Reactor的基本概念、框架和處理流程,并對基于Reactor模型的select,poll和epoll進行了比較分析后,再來對比看網絡編程框架就會更容易理解了。
?
2.?Libeasy網絡編程框架
Libeasy底層使用的是Libev事件庫,在分析Libeasy代碼前,首先對Libev有相關了解。
2.1?Libev簡介
l?Libev是什么?
Libev?is?an?event?loop:?you?register?interest?in?certain?events?(such?as?a?file?descriptor?being?readable?or?a?timeout?occurring),?and?it?will?manage?these?event?sources?and?provide?your?program?with?events.
Libev是一個event?loop:向libev注冊感興趣的events,比如Socket可讀事件,libev會對所注冊的事件的源進行管理,并在事件發生時觸發相應的程序。通過event?watcher來注冊事件
l?libev定義的watcher類型:
??ev_io??//?io?讀寫類型watcher
??ev_timer??//?定時器?類watcher
??ev_periodic
??ev_signal
??ev_child
??ev_stat
??ev_idle
??ev_prepare
??ev_check
??ev_embed
??ev_fork
??ev_cleanup
??ev_async??//?線程同步信號watcher
在libev中watcher還能支持優先級?
2.1.1?libev使用
下面以一個簡單例子程序說明libev的使用。這段程序實現從標準輸入異步讀取數據,5.5秒內沒有數據到來則超時的功能。
#include?<ev.h>
#include?<stdio.h>?
ev_io?stdin_watcher;
ev_timer?timeout_watcher;
//?all?watcher?callbacks?have?a?similar?signature
//?this?callback?is?called?when?data?is?readable?on?stdin
static?void?stdin_cb?(EV_P_?ev_io?*w,?int?revents)?{
??puts?("stdin?ready");
??//?for?one-shot?events,?one?must?manually?stop?the?watcher
??//?with?its?corresponding?stop?function.
??ev_io_stop?(EV_A_?w);
??//?this?causes?all?nested?ev_run's?to?stop?iterating
??ev_break?(EV_A_?EVBREAK_ALL);
}
//?another?callback,?this?time?for?a?time-out
static?void?timeout_cb?(EV_P_?ev_timer?*w,?int?revents)?{
??puts?("timeout");
??//?this?causes?the?innermost?ev_run?to?stop?iterating
??ev_break?(EV_A_?EVBREAK_ONE);
}
int?main?(void)?{
???//?use?the?default?event?loop?unless?you?have?special?needs
???struct?ev_loop?*loop?=?EV_DEFAULT;
???//?initialise?an?io?watcher,?then?start?it
???//?this?one?will?watch?for?stdin?to?become?readable
???ev_io_init?(&stdin_watcher,?stdin_cb,?/*STDIN_FILENO*/?0,?EV_READ);
???ev_io_start?(loop,?&stdin_watcher);
???//?initialise?a?timer?watcher,?then?start?it
???//?simple?non-repeating?5.5?second?timeout
???ev_timer_init?(&timeout_watcher,?timeout_cb,?5.5,?0.);
???ev_timer_start?(loop,?&timeout_watcher);
???//?now?wait?for?events?to?arrive
???ev_run?(loop,?0);
???//?break?was?called,?so?exit
???return?0;
}
2.1.2??Libev和Libevent比較
libevent和libev架構近似相同,對于非定時器類型,libevent使用雙向鏈表管理,而libev則是使用數組來管理。如我們所知,新的fd總是系統可用的最小fd,所以這個長度可以進行大小限制的,我們用一個連續的數組來存儲fd/watch?信息。如下圖,我們用anfd[fd]就可以找到對應的fd/watcher?信息,當然可能遇到anfd超出我們的buffer長度情形,這是我們用類似relloc?的函數來做數組遷移、擴大容量,但這種概率是很小的,所以其對系統性能的影響可以忽略不計。
?
我們用anfd[fd]找到的結構體中,有一個指向io_watch_list的頭指針,以epoll為例,當epoll_wait返回一個fd_event時?,我們就可以直接定位到對應fd的watch_list,這個watch_list的長度一般不會超過3?,fd_event會有一個導致觸發的事件,我們用這個事件依次和各個watch注冊的event做?“&”?操作,?如果不為0,則把對應的watch加入到待處理隊列pendings中(當我們啟用watcher優先級模式時,pendings是個2維數組,此時僅考慮普通模式)所以我們可以看到,這個操作是非常非常快。
?
再看添加watch的場景,把watch插入到相應的鏈表中,這個操作也是直接定位,然后在fdchange隊列中,加入對應的fd(如果這個fd已經被添加過,則不會發生這一步,我們通過anfd[fd]中一個bool?值來判斷)
注意,假如我們在某個fd上已經有個watch?注冊了read事件,這時我們又再添加一個watch,還是read?事件,但是不同的回調函數,在此種情況下,我們不應該調用epoll_ctrl?之類的系統調用,因為我們的events集合是沒有改變的,所以為了達到這個目的,anfd[fd]結構體中,還有一個events事件,它是原先的所有watcher的事件的“|”操作,向系統的epoll從新添加描述符的操作是在下次事件迭代開始前進行的,當我們依次掃描fdchangs,找到對應的anfd結構,如果發現先前的events與當前所有的watcher的“|”操作結果不等,則表示我們需要調用epoll_ctrl之類的函數來進行更改,反之不做操作,作為一條原則,在調用系統調用前,我們已經做了充分的檢查,確保不進行多余的系統調用。
再來看刪除和更新一個watcher造作,基于以上分析,這個操作也是近乎O(1)?的,當然,如果events事件更改,可能會發生一次系統調用。
所以我們對io?watcher的操作,在我們的用戶層面上,幾乎總是是O(1)的復雜度,當然如果牽涉到epoll?文件結構的更新,我們的系統調用?epoll_ctrl?在內核中還是?O(lgn)的復雜度,但我們已經在我們所能掌控的范圍內做到最好了。
2.1.3??性能測試對比
?
結論:The?cost?for?setting?up?or?changing?event?watchers?is?clearly?much?higher?for?libevent?than?for?libev,詳細性能對比測試參考這http://libev.schmorp.de/bench.html
2.2?libeasy
2.2.2?Server端使用
?
1、啟動流程
eio_?=?easy_eio_create(eio_,?io_thread_count_);
easy_eio_create(eio_,?io_thread_count_)做了如下幾件事:
1.?分配一個easy_pool_t的內存區,存放easy_io_t對象?
2.?設置一些tcp參數,比如tcp_nodelay(tcp_cork),cpu親核性等參數
3.?分配線程池的內存區并初始化
4.?對每個線程構建client_list,client_array,?初始化雙向鏈表conn_list?session_list?request_list
5.?設置listen?watcher的ev回調函數為easy_connection_on_listen
6.?調用easy_baseth_init初始化io線程
easy_listen_t*?listen?=?easy_connection_add_listen(eio_,?NULL,?port_,?&handler_);
1.?從eio->pool中為easy_listen_t和listen?watcher(在這里listen的watcher數默認為2個)分配空間
2.?開始監聽某個地址
3.?初始化每個read_watcher
4.?關注listen?fd的讀事件,設置其回調函數easy_connection_on_accep(在這里僅僅是初始化read_watcher,?還沒有激活,激活在每個IO線程啟動調用easy_io_on_thread_start的時候做。一旦激活后,當有連接到來的時候,觸發easy_connection_on_accept)
rc?=?easy_eio_start(eio_);
1.?調用pthread_create啟動每個io線程,線程執行函數easy_io_on_thread_start,在easy_io_on_thread_start中
a)?設置io線程的cpu親核性sched_setaffinity
b)?如果不是listen_all或者只有一個線程,則發出ev_async_send喚醒下一個線程的listen_watcher(實現連接請求的負載均衡)
2.?線程執行ev_run
easy_eio_wait(eio_);
調用pthead_join等待線程結束
2、處理流程
l?當連接到來時觸發easy_connection_on_accept
1.?調用accept獲得連接fd,構建connection(easy_connection_new),設置非阻塞,初始化connection參數和read、write、timeout的watcher
2.?切換listen線程,從自己切換到下一個io線程,調用ev_async_send激活下一個io線程的listen_watcher,實現負載均衡
3.?將connection加入到線程的connected_list線程列表中,并開啟該連接上的read、write、timeout的watcher
?
l?當數據包到來時觸發easy_connection_on_readable回調函數
1.?檢查當前IO線程同時正在處理的請求是否超過EASY_IOTH_DOING_REQ_CNT(8192),當前連接上的請求數是否超過EASY_CONN_DOING_REQ_CNT(1024),如果超過,則調用easy_connection_destroy(c)將連接銷毀掉,?提供了一種負載保護機制
2.?構建message空間
3.?調用read讀取socket數據
4.?作為服務端調用easy_connection_do_request
? ? a)?從message中解包
? ? b)?調用easy_connection_recycle_message看是否需要釋放老的message,構建新的message空間
? ? c)?調用hanler的process處理數據包,如果返回easy_ok則調用easy_connection_request_done
? ? d)?對發送數據進行打包
? ? e)?對返回碼是EASY_AGAIN的request將其放入session_list中
? ? f)?對返回碼是EASY_OK的request將其放入request_done_list中,更新統計計數
? ? g)?統計計數更新??
? ? h)?調用easy_connection_write_socket發送數據包
? ? i)?調用easy_connection_evio_start中ev_io_start(c->loop,?&c->read_watcher);開啟該連接的讀watcher
? ? j)?調用easy_connection_redispatch_thread進行負載均衡
如果負載均衡被禁或者該連接的message_list和output不為空,則直接返回,否則調用easy_thread_pool_rr從線程池中選擇一個io線程,將該連接從原來io線程上移除(停止讀寫timeout?的watcher),將該連接加入到新的io線程中的conn_list中,調用ev_async_send喚醒新的io線程,在easy_connection_on_wakeup中調用easy_connection_evio_start將該連接的read、write、timeou的watcher再打開。
?
l?當socket可寫時觸發easy_connection_on_writable回調函數:
1.?調用easy_connection_write_socket寫數據
2.?如果沒有數據可寫,將該連接的write_watcher停掉
?
2.2.3?客戶端使用
libeasy作為客戶端時,將每個發往libeasy服務器端的請求包封裝成一個session(easy_session_t),客戶端將這個session放入連接的隊列中然后返回,隨后收到包后,將相應的session從連接的發送隊列中刪除。詳細流程如下:
easy_session_t?*easy_session_create(int64_t?asize)
這個函數主要就做了一件事分配一個內存池easy_pool_t,在內存池頭部放置一個easy_session_t,剩下部分存放實際的數據包Packet,然后將session的type設置為EASY_TYPE_SESSION。
異步請求
int?easy_client_dispatch(easy_io_t?*eio,?easy_addr_t?addr,?easy_session_t?*s)
1.?根據socket?addr從線程池中選擇一個線程,將session加入該線程的session_list,然后將該線程喚醒
2.?線程喚醒后調用easy_connection_send_session_list
? ? ?a)??其中首先調用easy_connection_do_client,這里首先在該線程的client_list中查找該addr的client,如果沒找到,則新建一個client,初始化將其加入client_list,如果該client的connect未建立,調用easy_connection_do_connect建立該連接,然后返回該連接
? ? b)?easy_connection_do_connect中首先創建一個新的connection結構,和一個socket,設置非阻塞,并調用connect進行連接,初始化該連接的read、write、timeout?watcher(連接建立前是write,建立后是read)
? ? c)?調用easy_connection_session_build,其中調用encode函數對數據包進行打包,調用easy_hash_dlist_add(c->send_queue,?s->packet_id,?&s->send_queue_hash,?&s->send_queue_list)將這個session添加到連接的發送隊列中。這個函數將session添加到發送隊列的同時,同時將相應的項添加到hash表的相應的bucket的鏈表頭
? ? d)?開啟timeout?watcher?????
? ? e)?調用easy_connection_write_socket發送數據包?
?
l?當回復數據包到達觸發easy_connection_on_readable回調函數時
1.?初始化一個easy_message_t存放數據包
2.?從內核緩沖區讀入數據到應用層輸入緩沖區中,然后調用easy_connection_do_response進行處理
? ? a)?先解包,將該packet_id數據包從發包隊列中刪除,更新統計信息,停止timeout?watcher,
? ? b)?如果是同步請求,則調用session的process函數,從而調用easy_client_wait_process函數,喚醒客戶端接收數據包
l?當超時時間到還沒有收到回復數據包時觸發easy_connection_on_timeout_mesg回調函數
1.?從發送隊列中刪除請求數據包
2.?調用session的process函數,從而調用easy_client_wait_process函數,喚醒客戶端接
3.?釋放此連接
同步請求
void?*easy_client_send(easy_io_t?*eio,?easy_addr_t?addr,?easy_session_t?*s)
同步請求是通過異步請求實現的,easy_client_send方法封裝了異步請求接口easy_client_dispatch
1.?easy_client_send將session的process置為easy_client_wait_process方法
2.?初始化一個easy_client_wait_t?wobj
3.?調用easy_client_dispatch方法發送異步請求
4.?客戶端調用wait在wobj包裝的信號量上等待
5.?當這個請求收到包的時候觸發session的process函數,回調easy_client_wait_process方法,其中會給wobj發送信號喚醒客戶端,返回session封裝的請求的ipacket
?
2.2.4??特性總結
1.?多個IO線程/epoll,大大提升了數據包處理性能,特別是處理小數據包的性能
針對多核處理器,libeasy使用多個IO線程來充分發揮處理器性能,提升IO處理能力。特別是針對小數據包IO處理請求數較多的情況下,性能提升十分明顯。
2.?短任務和長任務區分,處理短任務更加高效(編碼了內存拷貝,線程切換)
同步處理
對于短任務而言,調用用戶process回調函數返回EASY_OK的數據包直接被加入該連接的發送隊列,發送給客戶端,這樣避免了數據包的內存拷貝和線程切換開銷。
異步處理
對于耗時較長的長任務而言,如果放在網絡庫的IO線程內執行,可能會阻塞住IO線程,所以需要異步處理。
?
3.?應用線程CPU親核性,避免線程調度開銷,提升處理性能
開啟親核特性將線程與指定CPU核進行綁定,避免了線程遷移導致的CPU?cache失效,同時它允許我們精確控制線程和cpu核的關系,從而根據需要劃分CPU核的使用。
sched_setaffinity(pid_t?pid,?unsigned?int?cpusetsize,?cpu_set_t?*mask)??
該函數設置進程為pid的這個進程,讓它運行在mask所設定的CPU上.如果pid的值為0,則表示指定的是當前進程,使當前進程運行在mask所設定的那些CPU上.第二個參數cpusetsize是mask所指定的數的長度.通常設定為sizeof(cpu_set_t).如果當前pid所指定的進程此時沒有運行在mask所指定的任意一個CPU上,則該指定的進程會從其它CPU上遷移到mask的指定的一個CPU上運行.?
4.?內存管理,減少小內存申請開銷,避免內存碎片化
Libeasy的內存管理和nginx一致,有興趣的可以去學習下,下面大致介紹其思想。
1)?創建一個內存池
2)?分配小塊內存(size?<=?max)
小塊內存分配模型:
?
上圖這個內存池模型是由上3個小內存池構成的,由于第一個內存池上剩余的內存不夠分配了,于是就創建了第二個新的內存池,第三個內存池是由于前面兩個內存池的剩余部分都不夠分配,所以創建了第三個內存池來滿足用戶的需求。由圖可見:所有的小內存池是由一個單向鏈表維護在一起的。這里還有兩個字段需要關注,failed和current字段。failed表示的是當前這個內存池的剩余可用內存不能滿足用戶分配請求的次數,如果下一個內存池也不能滿足,那么它的failed也會加1,直到滿足請求為止(如果沒有現成的內存池來滿足,會再創建一個新的內存池)。current字段會隨著failed的增加而發生改變,如果current指向的內存池的failed達到了一個閾值,current就指向下一個內存池了。
3)、大塊內存的分配(size?>?max)
大塊內存的分配請求不會直接在內存池上分配內存來滿足,而是直接向操作系統申請這么一塊內存(就像直接使用malloc分配內存一樣),然后將這塊內存掛到內存池頭部的large字段下。內存池的作用在于解決小塊內存池的頻繁申請問題,對于這種大塊內存,是可以忍受直接申請的。同樣,用圖形展示大塊內存申請模型:
?
4)、內存釋放
nginx利用了web?server應用的特殊場景來完成;一個web?server總是不停的接受connection和request,所以nginx就將內存池分了不同的等級,有進程級的內存池、connection級的內存池、request級的內存池。也就是說,創建好一個worker進程的時候,同時為這個worker進程創建一個內存池,待有新的連接到來后,就在worker進程的內存池上為該連接創建起一個內存池;連接上到來一個request后,又在連接的內存池上為request創建起一個內存池。這樣,在request被處理完后,就會釋放request的整個內存池,連接斷開后,就會釋放連接的內存池。
5)、總結
通過內存的分配和釋放可以看出,nginx只是將小塊內存的申請聚集到一起申請(內存池),然后一起釋放,避免了頻繁申請小內存,降低內存碎片的產生等問題。
5.?網絡流量自動負載均衡,充分發揮多核性能
1、在連接到來時,正在listen的IO線程接受連接,將其加入本線程的連接隊列中,之后主動喚醒下一個線程執行listen。通過切換listen線程來使每個線程上處理的連接數大致相同。
2、每一個連接上的流量是不同的,因此在每次有讀寫請求,計算該線程上近一段時間內請求速率,觸發負載均衡,將該連接移動到其它線程上,使每個線程處理的IO請求數大致相同。
6.?將encode和decode接口暴露給應用層,實現網絡編程框架與協議的分離
Libeasy將網絡數據包打包解包接口暴露給應用層,由用戶定義數據包內容的格式,實現了網絡編程框架與協議的分離,能夠支持http等其他協議類型,格式更改更加方便。
7.?底層采用libev,對于事件的注冊和更改速度更快
?
參考資料
1、?C10K?Problem
2、?Unix環境高級編程
3、?Unix網絡編程
4、?Nginx、Libevent
5、?Libevhttp://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#WHAT_TO_READ_WHEN_IN_A_HURRY
6、?Libeasy源碼分析等http://www.cnblogs.com/foxmailed/archive/2013/02/17/2908180.html
-------------------------------------------------------------------------------------
黑夜路人,一個關注開源技術、樂于學習、喜歡分享的程序員
博客:http://blog.csdn.net/heiyeshuwu
微博:http://weibo.com/heiyeluren
微信:heiyeluren2012 ?
與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
- 上一篇: CUDA在Windows下的软件开发环境
- 下一篇: Convolutional Neural