欧美性猛交xxxx免费看_牛牛在线视频国产免费_天堂草原电视剧在线观看免费_国产粉嫩高清在线观看_国产欧美日本亚洲精品一5区

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

教你如何區(qū)別select、poll、epoll?

Linux愛(ài)好者 ? 來(lái)源:Linux愛(ài)好者 ? 2023-11-21 15:25 ? 次閱讀

epoll和select

相比于select,epoll最大的好處在于它不會(huì)隨著監(jiān)聽(tīng)fd數(shù)目的增長(zhǎng)而降低效率。因?yàn)樵趦?nèi)核中的select實(shí)現(xiàn)中,它是采用輪詢來(lái)處理的,輪詢的fd數(shù)目越多,自然耗時(shí)越多。

并且,在linux/posix_types.h頭文件有這樣的聲明:

#define __FD_SETSIZE 1024

表示select最多同時(shí)監(jiān)聽(tīng)1024個(gè)fd,當(dāng)然,可以通過(guò)修改頭文件再重編譯內(nèi)核來(lái)擴(kuò)大這個(gè)數(shù)目,但這似乎并不治本。

一、IO多路復(fù)用的select

IO多路復(fù)用相對(duì)于阻塞式和非阻塞式的好處就是它可以監(jiān)聽(tīng)多個(gè) socket ,并且不會(huì)消耗過(guò)多資源。當(dāng)用戶進(jìn)程調(diào)用 select 時(shí),它會(huì)監(jiān)聽(tīng)其中所有 socket 直到有一個(gè)或多個(gè) socket 數(shù)據(jù)已經(jīng)準(zhǔn)備好,否則就一直處于阻塞狀態(tài)。select的缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,select()所維護(hù)的存儲(chǔ)大量文件描述符的數(shù)據(jù)結(jié)構(gòu),隨著文件描述符數(shù)量的增大,其復(fù)制的的開(kāi)銷也線性增長(zhǎng)。同時(shí),由于網(wǎng)絡(luò)響應(yīng)時(shí)間的延遲使得大量的tcp鏈接處于非?;钴S狀態(tài),但調(diào)用select()會(huì)對(duì)所有的socket進(jìn)行一次線性掃描,所以這也浪費(fèi)了一定的開(kāi)銷。不過(guò)它的好處還有就是它的跨平臺(tái)特性。

c008854c-716d-11ee-939d-92fbcf53809c.jpg

二、 epoll

epoll的ET是必須對(duì)非阻塞的socket才能工作,LT對(duì)于阻塞的socket也可以

所有I/O多路復(fù)用操作都是同步的,涵蓋select/poll。

阻塞/非阻塞是相對(duì)于同步I/O來(lái)說(shuō)的,與異步I/O無(wú)關(guān)。

select/poll/epoll本身是同步的,可以阻塞也可以不阻塞。

(阻塞和非阻塞 與同步不同步不同;阻塞與否 是自身,異步與否是與外部協(xié)作的關(guān)系)

skater:

無(wú)論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復(fù)用都是同步調(diào)用。因?yàn)樗鼈冊(cè)?read 調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間(epoll應(yīng)該是從mmap),過(guò)程都是需要等待的,也就是說(shuō)這個(gè)過(guò)程是同步的,如果內(nèi)核實(shí)現(xiàn)的拷貝效率不高,read 調(diào)用就會(huì)在這個(gè)同步過(guò)程中等待比較長(zhǎng)的時(shí)間。

c017d02e-716d-11ee-939d-92fbcf53809c.jpg

epoll事件:
   EPOLLIN : 表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
   EPOLLOUT: 表示對(duì)應(yīng)的文件描述符可以寫(xiě);
   EPOLLPRI: 表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
   EPOLLERR: 表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
   EPOLLHUP: 表示對(duì)應(yīng)的文件描述符被掛斷;

epoll高效的核心是:1、用戶態(tài)和內(nèi)核太共享內(nèi)存mmap。2、數(shù)據(jù)到來(lái)采用事件通知機(jī)制(而不需要輪詢)。

epoll的接口

epoll的接口非常簡(jiǎn)單,一共就三個(gè)函數(shù):

(1)epoll_create系統(tǒng)調(diào)用

epoll_create在C庫(kù)中的原型如下。

int epoll_create(int size);

epoll_create返回一個(gè)句柄,之后 epoll的使用都將依靠這個(gè)句柄來(lái)標(biāo)識(shí)。參數(shù) size是告訴 epoll所要處理的大致事件數(shù)目。不再使用 epoll時(shí),必須調(diào)用 close關(guān)閉這個(gè)句柄。

注意:size參數(shù)只是告訴內(nèi)核這個(gè) epoll對(duì)象會(huì)處理的事件大致數(shù)目,而不是能夠處理的事件的最大個(gè)數(shù)。在 Linux最新的一些內(nèi)核版本的實(shí)現(xiàn)中,這個(gè) size參數(shù)沒(méi)有任何意義。

(2)epoll_ctl系統(tǒng)調(diào)用

epoll_ctl在C庫(kù)中的原型如下。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epoll_ctl向 epoll對(duì)象中添加、修改或者刪除感興趣的事件,返回0表示成功,否則返回–1,此時(shí)需要根據(jù)errno錯(cuò)誤碼判斷錯(cuò)誤類型。epoll_wait方法返回的事件必然是通過(guò) epoll_ctl添加到 epoll中的。

參數(shù):

epfd: epoll_create返回的句柄,

op:的意義見(jiàn)下表:

EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;

EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;

EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;

fd:需要監(jiān)聽(tīng)的socket句柄fd,

event:告訴內(nèi)核需要監(jiān)聽(tīng)什么事的結(jié)構(gòu)體,struct epoll_event結(jié)構(gòu)如下:

epoll_data_t;
 
struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

__uint32_t events就要監(jiān)聽(tīng)的事件(感興趣的事件):

EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);

EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);

EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));

EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;

EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;

EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。

EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里

c022c074-716d-11ee-939d-92fbcf53809c.jpg

data成員是一個(gè)epoll_data聯(lián)合,其定義如下:
 
typedef union epoll_data {
 
void *ptr;
 
int fd;
 
uint32_t u32;
 
uint64_t u64;
 
} epoll_data_t;
 
可見(jiàn),這個(gè) data成員還與具體的使用方式相關(guān)。例如,ngx_epoll_module模塊只使用了聯(lián)合中的 ptr成員,
作為指向 ngx_connection_t連接的指針。我們?cè)陧?xiàng)目中一般使用的也是 ptr成員,因?yàn)樗梢灾赶蛉我獾慕Y(jié)構(gòu)
體地址。

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_wait在C庫(kù)中的原型如下:

int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);

收集在 epoll監(jiān)控的事件中已經(jīng)發(fā)生的事件,如果 epoll中沒(méi)有任何一個(gè)事件發(fā)生,則最多等待timeout毫秒后返回。epoll_wait的返回值表示當(dāng)前發(fā)生的事件個(gè)數(shù),如果返回0,則表示本次調(diào)用中沒(méi)有事件發(fā)生,如果返回–1,則表示出現(xiàn)錯(cuò)誤,需要檢查 errno錯(cuò)誤碼判斷錯(cuò)誤類型。

epfd:epoll的描述符。

events:分配好的 epoll_event結(jié)構(gòu)體數(shù)組,epoll將會(huì)把發(fā)生的事件復(fù)制到 events數(shù)組中(events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè) events數(shù)組中,不會(huì)去幫助我們?cè)谟脩魬B(tài)中分配內(nèi)存。內(nèi)核這種做法效率很高)。

maxevents:表示本次可以返回的最大事件數(shù)目,通常 maxevents參數(shù)與預(yù)分配的events數(shù)組的大小是相等的。

timeout:表示在沒(méi)有檢測(cè)到事件發(fā)生時(shí)最多等待的時(shí)間(單位為毫秒),如果 timeout為0,則表示 epoll_wait在 rdllist鏈表中為空,立刻返回,不會(huì)等待。

epoll有兩種工作模式:LT(水平觸發(fā))模式和ET(邊緣觸發(fā))模式。

默認(rèn)情況下,epoll采用 LT模式工作,這時(shí)可以處理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以將一個(gè)事件改為 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。

(水平觸發(fā)LT:當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)一次性全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它還會(huì)通知你在上次沒(méi)讀寫(xiě)完的文件描述符上繼續(xù)讀寫(xiě)

邊緣觸發(fā)ET:當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你

此可見(jiàn),水平觸發(fā)時(shí)如果系統(tǒng)中有大量你不需要讀寫(xiě)的就緒文件描述符,而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會(huì)充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見(jiàn)。)

如何來(lái)使用epoll

1、包含一個(gè)頭文件#include

2、create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄。

3、之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫(xiě)了。基本的語(yǔ)法為:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫(xiě)事件。max_events是當(dāng)前需要監(jiān)聽(tīng)的所有socket句柄數(shù)。

最后一個(gè)timeout:是epoll_wait的超時(shí),

為0的時(shí)候表示馬上返回,

為-1的時(shí)候表示一直等下去,直到有事件返回,

為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間,如果一直沒(méi)有事件,則返回。

一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來(lái)等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來(lái)保證主循環(huán)的效率。

epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件。

epoll通過(guò)在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹(shù))。把原先的select/poll調(diào)用分成了3個(gè)部分:

1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)

2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字

3)調(diào)用epoll_wait收集發(fā)生的事件的連接

epoll程序框架

幾乎所有的epoll程序都使用下面的框架:

偽代碼:

listenfd為全局變量,服務(wù)端監(jiān)聽(tīng)的套接字的fd。

關(guān)于epoll_wait返回值的一個(gè)簡(jiǎn)單測(cè)試
 
void test(int epollfd)
{
struct epoll_event events[MAX_EVENT_NUMBER];
int number;
 
while (1)
{
number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
printf("number : %2d

", number);
for (i = 0; i < number; i++)
    {
      sockfd = events[i].data.fd;
 
      if (sockfd == listenfd)
      {/*用戶上線*/
 
      }
      else if (events[i].events & EPOLLIN)
      {/*有數(shù)據(jù)可讀*/
 
      }
      else if (events[i].events & EPOLLOUT)
      {/*有數(shù)據(jù)可寫(xiě)*/
 
      }
      else
      {/*出錯(cuò)*/
 
      }
    }
  }
}
 
通過(guò)測(cè)試發(fā)現(xiàn)epoll_wait返回值number是不會(huì)大于MAX_EVENT_NUMBER的。
 
測(cè)試過(guò)程中,連接的客戶端數(shù)遠(yuǎn)大于MAX_EVENT_NUMBER,由此可以推論:epoll_wait()每次返回的是活躍客戶端的個(gè)數(shù),每次并將這些活躍的客戶端信息加入到events[MAX_EVENT_NUMBER]。
 
由此可見(jiàn),活躍客戶端的個(gè)數(shù)相同的情況下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函數(shù)執(zhí)行次數(shù)越少,但是events[MAX_EVENT_NUMBER]越大越消耗存儲(chǔ)資源。
 
所以,MAX_EVENT_NUMBER的選擇應(yīng)該在效率和資源間取一個(gè)平衡點(diǎn)。

示例代碼

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;ifd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發(fā)送數(shù)據(jù)
               
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
            }
            else
            {
                //其他的處理
            }
        }
    }

大致流程

 struct epoll_event ev, event_list[EVENT_MAX_COUNT];//ev用于注冊(cè)事件,event_list用于回傳要處理的事件
 
 listenfd = socket(AF_INET, SOCK_STREAM, 0);
 if(0 != bind(listenfd, (struct sockaddr *)
 if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定義了宏//#define LISTENQ   20  
 
 ev.data.fd = listenfd; //設(shè)置與要處理的事件相關(guān)的文件描述符
 ev.events = EPOLLIN | EPOLLET; //設(shè)置要處理的事件類型EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知
 
 
epfd = epoll_create(256);  //生成用于處理accept的epoll專用的文件描述符
//注冊(cè)epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實(shí)例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽(tīng)listenfd)
 
 
nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的發(fā)生

1. 首先熟悉下epoll的三個(gè)接口

int epoll_create(int size);

創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu),其最重要的是

1. 紅黑樹(shù), 用于存儲(chǔ)需要監(jiān)控的文件句柄以及事件

2. 就緒鏈表,用于存儲(chǔ)被觸發(fā)的文件句柄以及事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

用于設(shè)定,修改,或者刪除 監(jiān)控的文件句柄以及事件

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

阻塞等待timeout時(shí)間,如果文件句柄上相關(guān)事件被觸發(fā),則epoll_wait退出,并將觸發(fā)的事件 寫(xiě)入出參 events參數(shù),觸發(fā)的事件個(gè)數(shù)作為返回值返回

2. 如何使用這三個(gè)接口寫(xiě)一個(gè)server

首先使用epoll_create 創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu)

其次創(chuàng)建TCP socket 文件句柄acceptfd,綁定(ip:port),然后開(kāi)啟監(jiān)聽(tīng),并使用epoll_ctl 注冊(cè)到epoll中,監(jiān)聽(tīng)acceptfd句柄的EPOLL_IN事件(即可讀事件)

調(diào)用epoll_wait 開(kāi)始進(jìn)行阻塞等待

如果有客戶端連接過(guò)來(lái),則觸發(fā)acceptfd上的EPOLL_IN事件,epoll_wait返回后,可以得到觸發(fā)事件的信息, 這些信息其實(shí)就是一個(gè)struct epoll_event對(duì)象, 我們可以判斷這個(gè)epoll event對(duì)象fd是否和acceptfd一致,如果一致在認(rèn)為有新連接進(jìn)來(lái),則獲得新連接對(duì)應(yīng)的clientfd, 并使用epoll_ctl注冊(cè)到epoll, 監(jiān)控clientfd上的epoll_in事件,這個(gè)時(shí)候這個(gè)客戶端和服務(wù)器的連接就建立了

           typedef union epoll_data {
               void    *ptr;
               int      fd; //可以用fd, 也可以用ptr來(lái)保存事件對(duì)應(yīng)的文件句柄
               uint32_t u32;
               uint64_t u64;
           } epoll_data_t;
 
           struct epoll_event {
               uint32_t     events;    /* Epoll events */
               epoll_data_t data;      /* User data variable */
           };

用戶在客戶端輸入命令,將會(huì)觸發(fā)服務(wù)器端 clientfd上的epoll_in事件,epoll_wait返回觸發(fā)的event, 讀取event對(duì)應(yīng)的clientfd內(nèi)核緩沖區(qū)中的數(shù)據(jù),解析協(xié)議,執(zhí)行命令,得到返回結(jié)果,這個(gè)返回結(jié)果要返回給客戶端,則再使用epoll_ctl注冊(cè)clientfd的epoll_out事件到epoll,這個(gè)時(shí)候,我們會(huì)注意到clientfd上既有epoll_in,也有epoll_out,這樣其實(shí)沒(méi)有必要,客戶端在這個(gè)時(shí)候等待返回結(jié)果,不會(huì)再輸入命令,所以需要使用epoll_ctl把epoll_out刪除掉

如果clientfd內(nèi)核緩沖區(qū)可寫(xiě),epoll_wait這個(gè)時(shí)候會(huì)返回,并返回epoll_out事件,此時(shí)把返回的結(jié)果數(shù)據(jù)寫(xiě)入clientfd, 返回給客戶端

實(shí)例源碼

原文有相當(dāng)多的如錯(cuò)誤:

需要增加到頭文件和錯(cuò)誤修改

//'/0'->''
//bzero() 替換為memset (注意二者參數(shù)不一樣,bzero將前n個(gè)字節(jié)設(shè)為0,memset將前n 個(gè)字節(jié)的值設(shè)為值 c)
//local_addr 由char* 改為 string
#include 
#include  //atoi
#include  //memset
#include    //std:cout  等

修正后的源碼C++ lnux:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
//'/0'->''
//bzero() 替換為memset (注意二者參數(shù)不一樣,bzero將前n個(gè)字節(jié)設(shè)為0,memset將前n 個(gè)字節(jié)的值設(shè)為值 c)
//local_addr 由char* 改為 string
#include 
#include  //atoi
#include  //memset
#include    //std:cout  等

 
using namespace std;
 
#define MAXLINE   255              //讀寫(xiě)緩沖
#define OPEN_MAX  100
#define LISTENQ   20             //listen的第二個(gè)參數(shù)  定義TCP鏈接未完成隊(duì)列的大?。╨inux >2.6 則表示accpet之前的隊(duì)列)
#define SERV_PORT 5000
#define INFTIM    1000
 
#define TIMEOUT_MS      500
#define EVENT_MAX_COUNT 20
 
void setnonblocking(int sock)
{
    int opts;
    opts = fcntl(sock, F_GETFL);
    if(opts < 0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts | O_NONBLOCK;
    if(fcntl(sock, F_SETFL, opts) < 0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}
 
int main(int argc, char *argv[])
{
    int i, maxi, listenfd, connfd, sockfd, epfd, nfds, portnumber;
    ssize_t n;
    char line_buff[MAXLINE];
    
 
 
    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
            //fprintf()函數(shù)根據(jù)指定的format(格式)(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件
            //printf 將內(nèi)容發(fā)送到Default的輸出設(shè)備,通常為本機(jī)的顯示器,fprintf需要指定輸出設(shè)備,可以為文件,設(shè)備。
            //stderr
            return 1;
        }
    }
    else
    {
        fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
        return 1;
    }
 
    //聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊(cè)事件,數(shù)組用于回傳要處理的事件
    struct epoll_event ev, event_list[EVENT_MAX_COUNT];
 
    //生成用于處理accept的epoll專用的文件描述符
    epfd = epoll_create(256); //生成epoll文件描述符,既在內(nèi)核申請(qǐng)一空間,存放關(guān)注的socket fd上是否發(fā)生以及發(fā)生事件。size既epoll fd上能關(guān)注的最大socket fd數(shù)。隨你定好了。只要你有空間。
 
    struct sockaddr_in clientaddr;
    socklen_t clilenaddrLen;
    struct sockaddr_in serveraddr;
    
    listenfd = socket(AF_INET, SOCK_STREAM, 0);//Unix/Linux“一切皆文件”,創(chuàng)建(套接字)文件,id=listenfd
    if (listenfd < 0)
    {
       printf("socket error,errno %d:%s
",errno,strerror(errno));
    }
    //把socket設(shè)置為非阻塞方式
    //setnonblocking(listenfd);
 
    //設(shè)置與要處理的事件相關(guān)的文件描述符
    ev.data.fd = listenfd;
 
    //設(shè)置要處理的事件類型
    ev.events = EPOLLIN | EPOLLET; //EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知
    //ev.events=EPOLLIN;
 
    //注冊(cè)epoll事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實(shí)例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽(tīng)listenfd)
   
 
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family     = AF_INET;
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY轉(zhuǎn)換過(guò)來(lái)就是0.0.0.0,泛指本機(jī)的意思,也就是表示本機(jī)的所有IP*/
    serveraddr.sin_port       = htons(portnumber);
    
    if(0 != bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)))
     {
        printf("bind error,errno %d:%s
",errno,strerror(errno));
     }
 
     if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定義了宏
     {
         printf("listen error,errno %d:%s
",errno,strerror(errno));
     }
 
    maxi = 0;
 
    for ( ; ; )
    {
 
        //等待epoll事件的發(fā)生
        nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //epoll_wait(int epfd, struct epoll_event * event_list, int maxevents, int timeout),返回需要處理的事件數(shù)目
 
        //處理所發(fā)生的所有事件
        for(i = 0; i < nfds; ++i)
        {
            if(event_list[i].data.fd == listenfd) //如果新監(jiān)測(cè)到一個(gè)SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。
            {
                clilenaddrLen = sizeof(struct sockaddr_in);//在調(diào)用accept()前,要給addrLen賦值,這樣才不會(huì)出錯(cuò),addrLen = sizeof(clientaddr);或addrLen = sizeof(struct sockaddr_in);
                connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilenaddrLen);//(accpet詳解:https://blog.csdn.net/David_xtd/article/details/7087843)
                if(connfd < 0)
                {
                    //perror("connfd<0:connfd= %d",connfd);
                    printf("connfd<0,accept error,errno %d:%s
",errno,strerror(errno));
                    exit(1);
                }
 
                //setnonblocking(connfd);
 
                char *str = inet_ntoa(clientaddr.sin_addr);//將一個(gè)32位網(wǎng)絡(luò)字節(jié)序的二進(jìn)制IP地址轉(zhuǎn)換成相應(yīng)的點(diǎn)分十進(jìn)制的IP地址
 
                cout << "accapt a connection from " << str << endl;
 
                //設(shè)置用于讀操作的文件描述符
                ev.data.fd = connfd;
 
                //設(shè)置用于注測(cè)的讀操作事件
                ev.events = EPOLLIN | EPOLLET;
                //ev.events=EPOLLIN;
 
                //注冊(cè)ev
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //將accpet的句柄添加進(jìn)入(增加監(jiān)聽(tīng)的對(duì)象)
            }
            else if(event_list[i].events & EPOLLIN) //如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù),那么進(jìn)行讀入。
            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = event_list[i].data.fd) < 0)
                    continue;
 
                
                if ( (n = read(sockfd, line_buff, MAXLINE)) < 0)  //read時(shí)fd中的數(shù)據(jù)如果小于要讀取的數(shù)據(jù),就會(huì)引起阻塞?
                {
               //當(dāng)read()或者write()返回-1時(shí),一般要判斷errno
                    if (errno == ECONNRESET)//與客戶端的Socket被客戶端強(qiáng)行被斷開(kāi),而服務(wù)器還企圖read
                    {
                        close(sockfd);
                        event_list[i].data.fd = -1;
                    }
                    else
                        std::cout << "readline error" << std::endl;
                }
                else if (n == 0) //返回的n為0時(shí),說(shuō)明客戶端已經(jīng)關(guān)閉 
                {
                    close(sockfd);
                    event_list[i].data.fd = -1;
                }
 
                line_buff[n] = '';
                cout << "read " << line_buff << endl;
 
                //設(shè)置用于寫(xiě)操作的文件描述符
                ev.data.fd = sockfd;
 
                //設(shè)置用于注測(cè)的寫(xiě)操作事件
                ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);
 
                //修改sockfd上要處理的事件為EPOLLOUT
                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
 
            }
            else if(event_list[i].events & EPOLLOUT) // 如果有數(shù)據(jù)發(fā)送
            {
                sockfd = event_list[i].data.fd;
                write(sockfd, line_buff, n);
               
                //設(shè)置用于讀操作的文件描述符
                ev.data.fd = sockfd;
                
                //設(shè)置用于注測(cè)的讀操作事件
                ev.events = EPOLLIN | EPOLLET;
                
                //修改sockfd上要處理的事件為EPOLIN
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
            }
        }
    }
    return 0;
}

編譯命令

linux下編譯:g++ epoll.cpp -o epoll

命令行簡(jiǎn)單測(cè)試

curl 192.168.0.250:5000 -d "phone=123456789&name=Hwei"

相關(guān)知識(shí)

如何動(dòng)態(tài)的改變listen監(jiān)聽(tīng)的個(gè)數(shù)呢?

如果指定值在源代碼中是一個(gè)常值,那么增長(zhǎng)其大小需要重新編譯服務(wù)器程序。那么,我們可以為它設(shè)定一個(gè)缺省值,不過(guò)允許通過(guò)命令行選項(xiàng)或者環(huán)境變量來(lái)覆寫(xiě)該值。

void Listen(int fd, int backlog)
{
    char *ptr;
    
    if((ptr = getenv("LISTENQ")) != NULL)
        backlog = atoi(ptr);
 
    if(listen(fd, backlog) < 0)
        printf("listen error
");
}

隊(duì)列已滿的情況,如何處理?

當(dāng)一個(gè)客戶SYN到達(dá)時(shí),若這個(gè)隊(duì)列是滿的,TCP就忽略該分節(jié),也就是不會(huì)發(fā)送RST。

這么做的原因在于,隊(duì)列已滿的情況是暫時(shí)的,客戶TCP如果沒(méi)收收到RST,就會(huì)重發(fā)SYN,在隊(duì)列有空閑的時(shí)候處理該請(qǐng)求。如果服務(wù)器TCP立即響應(yīng)一個(gè)RST,客戶的connect調(diào)用就會(huì)立即返回一個(gè)錯(cuò)誤,強(qiáng)制應(yīng)用進(jìn)程處理這種情況,而不會(huì)再次重發(fā)SYN。而且客戶端也不無(wú)區(qū)別該套接口的狀態(tài),是“隊(duì)列已滿”還是“該端口沒(méi)有在監(jiān)聽(tīng)”。

SYN泛濫攻擊

向某一目標(biāo)服務(wù)器發(fā)送大量的SYN,用以填滿一個(gè)或多個(gè)TCP端口的未完成隊(duì)列。每個(gè)SYN的源IP地址都置成隨機(jī)數(shù)(IP欺騙),這樣防止攻擊服務(wù)器獲悉黑客的真實(shí)IP地址。通過(guò)偽造的SYN裝滿未完成連接隊(duì)列,使得合法的SYN不能排上隊(duì),導(dǎo)致針對(duì)合法用戶的服務(wù)被拒絕。

防御方法:

針對(duì)服務(wù)器主機(jī)的方法。增加連接緩沖隊(duì)列長(zhǎng)度和縮短連接請(qǐng)求占用緩沖隊(duì)列的超時(shí)時(shí)間。該方式最簡(jiǎn)單,被很多操作系統(tǒng)采用,但防御性能也最弱。

針對(duì)路由器過(guò)濾的方法。由于DDoS攻擊,包括SYN-Flood,都使用地址偽裝技術(shù),所以在路由器上使用規(guī)則過(guò)濾掉被認(rèn)為地址偽裝的包,會(huì)有效的遏制攻擊流量。

針對(duì)防火墻的方法。在SYN請(qǐng)求連接到真正的服務(wù)器之前,使用基于防火墻的網(wǎng)關(guān)來(lái)測(cè)試其合法性。它是一種被普遍采用的專門針對(duì)SYN-Flood攻擊的防御機(jī)制。

SYN:同步序列編號(hào)(Synchronize Sequence Numbers)

c026ed2a-716d-11ee-939d-92fbcf53809c.jpg

它們的含義是:
SYN表示建立連接,
FIN表示關(guān)閉連接,
ACK表示響應(yīng),
PSH表示有 
DATA數(shù)據(jù)傳輸,
RST表示連接重置。

SYN(synchronous建立聯(lián)機(jī))

ACK(acknowledgement 確認(rèn))

PSH(push傳送)

FIN(finish結(jié)束)

RST(reset重置)

URG(urgent緊急)

Sequence number(順序號(hào)碼)

Acknowledge number(確認(rèn)號(hào)碼)

三次握手:

在TCP/IP協(xié)議中,TCP協(xié)議提供可靠的連接服務(wù),采用三次握手建立一個(gè)連接。
 第一次握手:建立連接時(shí),客戶端發(fā)送syn包(syn=j)到服務(wù)器,并進(jìn)入SYN_SEND狀態(tài),等待服務(wù)器確認(rèn);
第二次握手:服務(wù)器收到syn包,必須確認(rèn)客戶的SYN(ack=j+1),同時(shí)自己也發(fā)送一個(gè)SYN包(syn=k),即SYN+ACK包,此時(shí)服務(wù)器進(jìn)入SYN_RECV狀態(tài);
 第三次握手:客戶端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=k+1),此包發(fā)送完畢,客戶端和服務(wù)器進(jìn)入ESTABLISHED狀態(tài),完成三次握手。完成三次握手,客戶端與服務(wù)器開(kāi)始傳送數(shù)據(jù).

第一次握手:主機(jī)A發(fā)送位碼為syn=1,隨機(jī)產(chǎn)生seq number=1234567的數(shù)據(jù)包到服務(wù)器,主機(jī)B由SYN=1知道,A要求建立聯(lián)機(jī);

第二次握手:主機(jī)B收到請(qǐng)求后要確認(rèn)聯(lián)機(jī)信息,向A發(fā)送ack number=(主機(jī)A的seq+1),syn=1,ack=1,隨機(jī)產(chǎn)生seq=7654321的包;

第三次握手:主機(jī)A收到后檢查ack number是否正確,即第一次發(fā)送的seq number+1,以及位碼ack是否為1,若正確,主機(jī)A會(huì)再發(fā)送ack number=(主機(jī)B的seq+1),ack=1,主機(jī)B收到后確認(rèn)seq值與ack=1則連接建立成功。

實(shí)例代碼二

多進(jìn)程Epoll:

#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#define PROCESS_NUM 10  
static int  
create_and_bind (char *port)  
{  
    int fd = socket(PF_INET, SOCK_STREAM, 0);  
    struct sockaddr_in serveraddr;  
    serveraddr.sin_family = AF_INET;  
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    serveraddr.sin_port = htons(atoi(port));  
    bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));  
    return fd;  
}  
    static int  
make_socket_non_blocking (int sfd)  
{  
    int flags, s;  
 
    flags = fcntl (sfd, F_GETFL, 0);  
    if (flags == -1)  
    {  
        perror ("fcntl");  
        return -1;  
    }  
 
    flags |= O_NONBLOCK;  
    s = fcntl (sfd, F_SETFL, flags);  
    if (s == -1)  
    {  
        perror ("fcntl");  
        return -1;  
    }  
 
    return 0;  
}  
  
#define MAXEVENTS 64  
 
int  
main (int argc, char *argv[])  
{  
    int sfd, s;  
    int efd;  
    struct epoll_event event;  
    struct epoll_event *events;  
 
    sfd = create_and_bind("1234");  
    if (sfd == -1)  
        abort ();  
 
    s = make_socket_non_blocking (sfd);  
    if (s == -1)  
        abort ();  
 
    s = listen(sfd, SOMAXCONN);  
    if (s == -1)  
    {  
        perror ("listen");  
        abort ();  
    }  
 
    efd = epoll_create(MAXEVENTS);  
    if (efd == -1)  
    {  
        perror("epoll_create");  
        abort();  
    }  
 
    event.data.fd = sfd;  
    //event.events = EPOLLIN | EPOLLET;  
    event.events = EPOLLIN;  
    s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);  
    if (s == -1)  
    {  
        perror("epoll_ctl");  
        abort();  
    }  
 
    /* Buffer where events are returned */  
    events = calloc(MAXEVENTS, sizeof event);  
            int k;  
    for(k = 0; k < PROCESS_NUM; k++)  
    {  
        int pid = fork();  
        if(pid == 0)  
        {  
 
            /* The event loop */  
            while (1)  
            {  
                int n, i;  
                n = epoll_wait(efd, events, MAXEVENTS, -1);  
                printf("process %d return from epoll_wait!
", getpid());  
                                       /* sleep here is very important!*/  
                //sleep(2);  
                                       for (i = 0; i < n; i++)  
                {  
                    if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)
                                                    || (!(events[i].events & EPOLLIN)))  
                    {  
                        /* An error has occured on this fd, or the socket is not  
                        ready for reading (why were we notified then?) */  
                        fprintf (stderr, "epoll error
");  
                        close (events[i].data.fd);  
                        continue;  
                    }  
                    else if (sfd == events[i].data.fd)  
                    {  
                        /* We have a notification on the listening socket, which  
                        means one or more incoming connections. */  
                        struct sockaddr in_addr;  
                        socklen_t in_len;  
                        int infd;  
                        char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];  
 
                        in_len = sizeof in_addr;  
                        infd = accept(sfd, &in_addr, &in_len);  
                        if (infd == -1)  
                        {  
                            printf("process %d accept failed!
", getpid());  
                            break;  
                        }  
                        printf("process %d accept successed!
", getpid());  
 
                        /* Make the incoming socket non-blocking and add it to the  
                        list of fds to monitor. */  
                        close(infd); 
                    }  
                }  
            }  
        }  
    }  
    int status;  
    wait(&status);  
    free (events);  
    close (sfd);  
    return EXIT_SUCCESS;  
}  

建立2000+個(gè)鏈接的測(cè)試代碼

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include
#include 
#include 
 
const int MAXLINE = 5;
int count = 1;
 
static int make_socket_non_blocking(int fd)
{
  int flags, s;
 
  flags = fcntl (fd, F_GETFL, 0);
  if (flags == -1)
    {
      perror ("fcntl");
      return -1;
    }
 
  flags |= O_NONBLOCK;
  s = fcntl (fd, F_SETFL, flags);
  if (s == -1)
    {
      perror ("fcntl");
      return -1;
    }
 
  return 0;
}
 
void sockconn()
{
int sockfd;
struct sockaddr_in server_addr;
struct hostent *host;
char buf[100];
unsigned int value = 1;
 
host = gethostbyname("127.0.0.1");
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error
");
return;
}

//setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));

//make_socket_non_blocking(sockfd);
 
bzero(&server_addr, sizeof(server_addr));
 
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr = *((struct in_addr*) host->h_addr);
 
int cn = connect(sockfd, (struct sockaddr *) &server_addr,
sizeof(server_addr));
if (cn == -1) {
printf("connect error errno=%d
", errno);
return;
 
}
//char *buf = "h";
sprintf(buf, "%d", count);
count++;
write(sockfd, buf, strlen(buf));
close(sockfd);

printf("client send %s
", buf);

return;
}
 
int main(void) {
 
 int i;
 for (i = 0; i < 2000; i++)
 {
   sockconn();
 }
 
 return 0;
}

關(guān)于ET、LT兩種工作模式

水平觸發(fā)LT:

其中LT就是與select和poll類似,當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)一次性全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它還會(huì)通知你在上次沒(méi)讀寫(xiě)完的文件描述符上繼續(xù)讀寫(xiě)

邊緣觸發(fā)ET:

當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你

水平觸發(fā):只要緩沖區(qū)有數(shù)據(jù)就會(huì)一直觸發(fā)

邊沿觸發(fā):只有在緩沖區(qū)增加數(shù)據(jù)的那一刻才會(huì)觸發(fā)

由此可見(jiàn),水平觸發(fā)時(shí)如果系統(tǒng)中有大量你不需要讀寫(xiě)的就緒文件描述符(有些fd有數(shù)據(jù),但是你不處理那些fd),而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會(huì)充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見(jiàn)。

c02ef88a-716d-11ee-939d-92fbcf53809c.jpg

c03dc0a4-716d-11ee-939d-92fbcf53809c.jpg

4、關(guān)于ET、LT兩種工作模式

可以得出這樣的結(jié)論:

ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說(shuō),如果要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒(méi)有處理就會(huì)一直通知下去的.

epoll中讀寫(xiě)數(shù)據(jù) 的注意事項(xiàng)

在一個(gè)非阻塞的socket上調(diào)用read/write函數(shù),返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。

從字面上看,意思是:

EAGAIN: 再試一次

EWOULDBLOCK:如果這是一個(gè)阻塞socket, 操作將被block

perror輸出:Resource temporarily unavailable

總結(jié):

這個(gè)錯(cuò)誤表示資源暫時(shí)不夠,可能read時(shí), 讀緩沖區(qū)沒(méi)有數(shù)據(jù), 或者write時(shí),寫(xiě)緩沖區(qū)滿了。

遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 時(shí)errno設(shè)置為EAGAIN。

所以對(duì)于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯(cuò)了。但對(duì)于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯(cuò)了??赡苁荝esource temporarily unavailable。這時(shí)你應(yīng)該再試,直到Resource available。

本文主要講述epoll模型(不完全是針對(duì)epoll)下讀寫(xiě)數(shù)據(jù)接口使用的注意事項(xiàng)

1、read write

函數(shù)原型如下:

#include 
ssize_t read(int filedes, void* buf, size_t nbytes)
ssize_t write(int filedes, const void* buf, size_t nbytes)

其中,read返回實(shí)際讀取到的字節(jié)數(shù)。但實(shí)際讀取的字節(jié)很有可能少于指定要讀取的字節(jié)數(shù)nbytes。因此會(huì)分為:

①返回值大于0。 讀取正常,返回實(shí)際讀取到的字節(jié)數(shù)

②返回值等于0。 讀取異常,讀取到文件filedes結(jié)尾處了。這里邏輯上要理解為read已經(jīng)讀取完數(shù)據(jù)

③返回值小于0(-1)。 讀取出錯(cuò),在處理網(wǎng)絡(luò)請(qǐng)求時(shí)可能是網(wǎng)絡(luò)異常。著重注意當(dāng)返回-1,此時(shí)errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對(duì)應(yīng)的讀緩沖區(qū)為空

而write返回的實(shí)際寫(xiě)入字節(jié)數(shù)正常情況是與制定寫(xiě)入的字節(jié)數(shù)nbytes相同的,不相等說(shuō)明寫(xiě)入異常了,著重注意,此時(shí)errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對(duì)應(yīng)的寫(xiě)緩沖區(qū)為空。注,EAGAIN等同于EWOULLDBLOCK。

總之,這個(gè)錯(cuò)誤表示資源暫時(shí)不夠,可能read時(shí)讀緩沖區(qū)沒(méi)有數(shù)據(jù), 或者write時(shí)寫(xiě)緩沖區(qū)滿了。遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同時(shí)errno設(shè)置為EAGAIN。

所以對(duì)于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯(cuò)了。但對(duì)于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯(cuò)了。可能支持緩沖區(qū)空或者滿,這時(shí)應(yīng)該再試,直到Resource available。

綜上,對(duì)于非阻塞的socket,正確的讀寫(xiě)操作為:

LT模式

讀: 忽略掉errno = EAGAIN的錯(cuò)誤,下次繼續(xù)讀;

寫(xiě):忽略掉errno = EAGAIN的錯(cuò)誤,下次繼續(xù)寫(xiě)。

對(duì)于select和epoll的LT模式,這種讀寫(xiě)方式是沒(méi)有問(wèn)題的。但對(duì)于epoll的ET模式,這種方式還有漏洞。

下面來(lái)介紹下epoll事件的兩種模式LT(水平觸發(fā))和ET(邊沿觸發(fā)),根據(jù)可以理解為,文件描述符的讀寫(xiě)狀態(tài)發(fā)生變化才會(huì)觸發(fā)epoll事件,具體說(shuō)來(lái)如下:二者的差異在于 level-trigger 模式下只要某個(gè) socket 處于 readable/writable 狀態(tài),無(wú)論什么時(shí)候進(jìn)行 epoll_wait 都會(huì)返回該 socket;而 edge-trigger 模式下只有某個(gè) socket 從 unreadable 變?yōu)?readable,或從unwritable 變?yōu)閣ritable時(shí),epoll_wait 才會(huì)返回該 socket。如下兩個(gè)示意圖:

從socket讀數(shù)據(jù):

c02ef88a-716d-11ee-939d-92fbcf53809c.jpg

往socket寫(xiě)數(shù)據(jù):

c03dc0a4-716d-11ee-939d-92fbcf53809c.jpg

所以在epoll的ET模式下,正確的讀寫(xiě)方式為:

讀: 只要可讀, 就一直讀,直到返回0,或者 errno = EAGAIN寫(xiě):只要可寫(xiě), 就一直寫(xiě),直到數(shù)據(jù)發(fā)送完,或者 errno = EAGAIN

這里的意思是,對(duì)于ET模式,相當(dāng)于我們要自己重寫(xiě)read和write,使其像”原子操作“一樣,保證一次read 或 write能夠完整的讀完緩沖區(qū)的數(shù)據(jù)或者寫(xiě)完要寫(xiě)入緩沖區(qū)的數(shù)據(jù)。因此,實(shí)現(xiàn)為用while包住read和write即可。但是對(duì)于select或者LT模式,我們可以只使用一次read和write,因?yàn)樵谥鞒绦蛑袝?huì)一直while,而事件再下一次select時(shí)還會(huì)被獲取到。但也可以實(shí)現(xiàn)為用while包住read和write。從邏輯上講,一次性把數(shù)據(jù)讀取完整可以保證數(shù)據(jù)的完整性。

下面來(lái)說(shuō)明這種”原子操作“read和write

int n = 0;
while(1)
{
    nread = read(fd, buf + n, BUFSIZ - 1); //讀時(shí),用戶進(jìn)程指定的接收數(shù)據(jù)緩沖區(qū)大小固定,一般要比數(shù)據(jù)大
    if(nread < 0)
    {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
            continue;
        }
        else
        {
            break; //or return;
        }
    }
    else if(nread == 0)
    {
        break; //or return. because read the EOF
    }
    else
    {
        n += nread;
    }
}
int data_size = strlen(buf);
int n = 0;
while(1)
{
    nwrite = write(fd, buf + n, data_size);//寫(xiě)時(shí),數(shù)據(jù)大小一直在變化
    if(nwrite < data_size)
    {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
            continue;
        }
        else
        {
            break;//or return;        
        }
 
    }
    else
    {
        n += nwrite;
        data_size -= nwrite;
    }
}

正確的accept,accept 要考慮 2 個(gè)問(wèn)題:

(1) LT模式下或ET模式下,阻塞的監(jiān)聽(tīng)socket, accept 存在的問(wèn)題

accept每次都是從已經(jīng)完成三次握手的tcp隊(duì)列中取出一個(gè)連接,考慮這種情況: TCP 連接被客戶端夭折,即在服務(wù)器調(diào)用 accept 之前,客戶端主動(dòng)發(fā)送 RST 終止連接,導(dǎo)致剛剛建立的連接從就緒隊(duì)列中移出,如果套接口被設(shè)置成阻塞模式,服務(wù)器就會(huì)一直阻塞在 accept 調(diào)用上,直到其他某個(gè)客戶建立一個(gè)新的連接為止。但是在此期間,服務(wù)器單純地阻塞在accept 調(diào)用上,就緒隊(duì)列中的其他描述符都得不到處理。

解決辦法是:把監(jiān)聽(tīng)套接口設(shè)置為非阻塞,當(dāng)客戶在服務(wù)器調(diào)用 accept 之前中止某個(gè)連接時(shí),accept 調(diào)用可以立即返回 -1, 這時(shí)源自 Berkeley 的實(shí)現(xiàn)會(huì)在內(nèi)核中處理該事件,并不會(huì)將該事件通知給 epoll,而其他實(shí)現(xiàn)把 errno 設(shè)置為 ECONNABORTED 或者 EPROTO 錯(cuò)誤,我們應(yīng)該忽略這兩個(gè)錯(cuò)誤。

(2) ET 模式下 accept 存在的問(wèn)題

考慮這種情況:多個(gè)連接同時(shí)到達(dá),服務(wù)器的 TCP 就緒隊(duì)列瞬間積累多個(gè)就緒連接,由于是邊緣觸發(fā)模式,epoll 只會(huì)通知一次,accept 只處理一個(gè)連接,導(dǎo)致 TCP 就緒隊(duì)列中剩下的連接都得不到處理。

解決辦法是:將監(jiān)聽(tīng)套接字設(shè)置為非阻塞模式,用 while 循環(huán)抱住 accept 調(diào)用,處理完 TCP 就緒隊(duì)列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊(duì)列中的所有連接呢? accept 返回 -1 并且 errno 設(shè)置為 EAGAIN 就表示所有連接都處理完。

綜合以上兩種情況,服務(wù)器應(yīng)該使用非阻塞地 accept, accept 在 ET 模式下 的正確使用方式為:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,   
                (size_t *)&addrlen)) > 0) {  
    handle_client(conn_sock);  
}  
if (conn_sock == -1) {  
    if (errno != EAGAIN && errno != ECONNABORTED   
            && errno != EPROTO && errno != EINTR)   
        perror("accept");  
}

一道騰訊后臺(tái)開(kāi)發(fā)的面試題:

使用Linux epoll模型,水平觸發(fā)模式;當(dāng)socket可寫(xiě)時(shí),會(huì)不停的觸發(fā) socket 可寫(xiě)的事件,如何處理?

第一種最普遍的方式:

需要向 socket 寫(xiě)數(shù)據(jù)的時(shí)候才把 socket 加入 epoll ,等待可寫(xiě)事件。接受到可寫(xiě)事件后,調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。當(dāng)所有數(shù)據(jù)都寫(xiě)完后,把 socket 移出 epoll。

這種方式的缺點(diǎn)是,即使發(fā)送很少的數(shù)據(jù),也要把 socket 加入 epoll,寫(xiě)完后在移出 epoll,有一定操作代價(jià)。

一種改進(jìn)的方式:

開(kāi)始不把 socket 加入 epoll,需要向 socket 寫(xiě)數(shù)據(jù)的時(shí)候,直接調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驅(qū)動(dòng)下寫(xiě)數(shù)據(jù),全部數(shù)據(jù)發(fā)送完畢后,再移出 epoll。

這種方式的優(yōu)點(diǎn)是:數(shù)據(jù)不多的時(shí)候可以避免 epoll 的事件處理,提高效率。

多路復(fù)用

如文初的說(shuō)明表示,這三者都是 I/O 多路復(fù)用機(jī)制,且簡(jiǎn)要介紹了多路復(fù)用的定義,那么如何更加直觀地了解多路復(fù)用呢?這里有張圖:

c0759f2e-716d-11ee-939d-92fbcf53809c.jpg

對(duì)于網(wǎng)頁(yè)服務(wù)器 Nginx 來(lái)說(shuō),會(huì)有很多連接進(jìn)來(lái), epoll 會(huì)把他們都監(jiān)視起來(lái),然后像撥開(kāi)關(guān)一樣,誰(shuí)有數(shù)據(jù)就撥向誰(shuí),然后調(diào)用相應(yīng)的代碼處理。

一般來(lái)說(shuō)以下場(chǎng)合需要使用 I/O 多路復(fù)用:

當(dāng)客戶處理多個(gè)描述字時(shí)(一般是交互式輸入和網(wǎng)絡(luò)套接口)

如果一個(gè)服務(wù)器既要處理 TCP,又要處理 UDP,一般要使用 I/O 復(fù)用

如果一個(gè) TCP 服務(wù)器既要處理監(jiān)聽(tīng)套接口,又要處理已連接套接口

為什么epoll可以支持百萬(wàn)級(jí)別的連接?

在server的處理過(guò)程中,大家可以看到其中重要的操作是,使用epoll_ctl修改clientfd在epoll中注冊(cè)的epoll_event, 這個(gè)操作首先在紅黑樹(shù)中找到fd對(duì)應(yīng)的epoll_event, 然后進(jìn)行修改,紅黑樹(shù)是典型的二叉平衡樹(shù),其時(shí)間復(fù)雜度是log2(n), 1百萬(wàn)的文件句柄,只需要16次左右的查找,速度是非??斓?,支持百萬(wàn)級(jí)別毫無(wú)壓力

另外,epoll通過(guò)注冊(cè)fd上的回調(diào)函數(shù),回調(diào)函數(shù)監(jiān)控到有事件發(fā)生,則準(zhǔn)備好相關(guān)的數(shù)據(jù)放到到就緒鏈表里面去,這個(gè)動(dòng)作非??欤杀疽卜浅P?/p>

socket讀寫(xiě)返回值的處理

在調(diào)用socket讀寫(xiě)函數(shù)read(),write()時(shí),都會(huì)有返回值。如果沒(méi)有正確處理返回值,就可能引入一些問(wèn)題

總結(jié)了以下幾點(diǎn)

1當(dāng)read()或者write()函數(shù)返回值大于0時(shí),表示實(shí)際從緩沖區(qū)讀取或者寫(xiě)入的字節(jié)數(shù)目

2當(dāng)read()函數(shù)返回值為0時(shí),表示對(duì)端已經(jīng)關(guān)閉了 socket,這時(shí)候也要關(guān)閉這個(gè)socket,否則會(huì)導(dǎo)致socket泄露。netstat命令查看下,如果有closewait狀態(tài)的socket,就是socket泄露了

當(dāng)write()函數(shù)返回0時(shí),表示當(dāng)前寫(xiě)緩沖區(qū)已滿,是正常情況,下次再來(lái)寫(xiě)就行了。

3當(dāng)read()或者write()返回-1時(shí),一般要判斷errno

如果errno == EINTR,表示系統(tǒng)當(dāng)前中斷了,直接忽略

如果errno == EAGAIN或者EWOULDBLOCK,非阻塞socket直接忽略;如果是阻塞的socket,一般是讀寫(xiě)操作超時(shí)了,還未返回。這個(gè)超時(shí)是指socket的SO_RCVTIMEO與SO_SNDTIMEO兩個(gè)屬性。所以在使用阻塞socket時(shí),不要將超時(shí)時(shí)間設(shè)置的過(guò)小。不然返回了-1,你也不知道是socket連接是真的斷開(kāi)了,還是正常的網(wǎng)絡(luò)抖動(dòng)。一般情況下,阻塞的socket返回了-1,都需要關(guān)閉重新連接。

4.另外,對(duì)于非阻塞的connect,可能返回-1.這時(shí)需要判斷errno,如果 errno == EINPROGRESS,表示正在處理中,否則表示連接出錯(cuò)了,需要關(guān)閉重連。之后使用select,檢測(cè)到該socket的可寫(xiě)事件時(shí),要判斷getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen),看socket是否出錯(cuò)了。如果err值為0,則表示connect成功;否則也應(yīng)該關(guān)閉重連

5 在使用epoll時(shí),有ET與LT兩種模式。ET模式下,socket需要read或者write到返回-1為止。對(duì)于非阻塞的socket沒(méi)有問(wèn)題,但是如果是阻塞的socket,正如第三條中所說(shuō)的,只有超時(shí)才會(huì)返回。所以在ET模式下千萬(wàn)不要使用阻塞的socket。那么LT模式為什么沒(méi)問(wèn)題呢?一般情況下,使用LT模式,我們只要調(diào)用一次read或者write函數(shù),如果沒(méi)有讀完或者沒(méi)有寫(xiě)完,下次再來(lái)就是了。由于已經(jīng)返回了可讀或者可寫(xiě)事件,所以可以保證調(diào)用一次read或者write會(huì)正常返回。

nread為-1且errno==EAGAIN,說(shuō)明數(shù)據(jù)已經(jīng)讀完,設(shè)置EPOLLOUT。

網(wǎng)絡(luò)狀態(tài)查詢命令

sar、iostat、lsof

問(wèn)題記錄

客戶端

1、Cannot assign requested address

c07ec536-716d-11ee-939d-92fbcf53809c.jpg

大致上是由于客戶端頻繁的連服務(wù)器,由于每次連接都在很短的時(shí)間內(nèi)結(jié)束,導(dǎo)致很多的TIME_WAIT,以至于用光了可用的端 口號(hào),所以新的連接沒(méi)辦法綁定端口,即“Cannot assign requested address”。是客戶端的問(wèn)題不是服務(wù)器端的問(wèn)題。通過(guò)netstat,的確看到很多TIME_WAIT狀態(tài)的連接。

client端頻繁建立連接,而端口釋放較慢,導(dǎo)致建立新連接時(shí)無(wú)可用端口。

netstat -a|grep TIME_WAIT
tcp        0      0 e100069210180.zmf:49477     e100069202104.zmf.tbs:websm TIME_WAIT   
tcp        0      0 e100069210180.zmf:49481     e100069202104.zmf.tbs:websm TIME_WAIT   
tcp        0      0 e100069210180.zmf:49469     e100069202104.zmf.tbs:websm TIME_WAIT   
……

解決辦法

執(zhí)行命令修改如下內(nèi)核參數(shù) (需要root權(quán)限)

調(diào)低端口釋放后的等待時(shí)間,默認(rèn)為60s,修改為15~30s:

sysctl -w net.ipv4.tcp_fin_timeout=30

修改tcp/ip協(xié)議配置, 通過(guò)配置/proc/sys/net/ipv4/tcp_tw_resue, 默認(rèn)為0,修改為1,釋放TIME_WAIT端口給新連接使用:

sysctl -w net.ipv4.tcp_timestamps=1

修改tcp/ip協(xié)議配置,快速回收socket資源,默認(rèn)為0,修改為1:

sysctl -w net.ipv4.tcp_tw_recycle=1

允許端口重用:

sysctl -w net.ipv4.tcp_tw_reuse = 1

2、2.8萬(wàn)左右的鏈接,報(bào)錯(cuò)誤(可能端口用盡)

如果沒(méi)有TIME_WAIT 狀態(tài)的連接,那有可能端口用盡,特別是長(zhǎng)連接的時(shí)候,查看開(kāi)放的端口范圍:

[root@VM_0_8_centos usr]# sysctl -a |grep port_range
net.ipv4.ip_local_port_range = 32768    60999
sysctl: reading key "net.ipv6.conf.all.stable_secret"
sysctl: reading key "net.ipv6.conf.default.stable_secret"
sysctl: reading key "net.ipv6.conf.eth0.stable_secret"
sysctl: reading key "net.ipv6.conf.lo.stable_secret"
[root@VM_0_8_centos usr]#

60999 - 32768 = 28,231,剛好2.8萬(wàn),長(zhǎng)連接把端口用光了。修改端口開(kāi)放范圍:

vi /etc/sysctl.conf net.ipv4.ip_local_port_range = 10000 65535

執(zhí)行sysctl -p 使得生效

服務(wù)端

影響鏈接不往上走的原因:

1、端口用完了 (客戶端,

查看端口范圍sysctl -a |grep port_range,返回:
net.ipv4.ip_local_port_range = 32768 60999,所以可用端口是60999-32768 =2.8w )

2、文件fd用完了

3、內(nèi)存用完了

4、網(wǎng)絡(luò)

5、配置:fsfile-max = 1048576 #文件fd的最大值,

fd=open(),fd從3開(kāi)始,0:stdin標(biāo)準(zhǔn)輸入1:stdout標(biāo)準(zhǔn)輸出 2:stderr錯(cuò)誤輸出

Epoll 難以解決的問(wèn)題

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 還是要從這個(gè)api說(shuō)起,這個(gè)api 可以監(jiān)聽(tīng)很多個(gè)fd,但是timeout 只有一個(gè)。 有這一個(gè)場(chǎng)景: 你想關(guān)注 fd == 10的這個(gè)描述符,你希望該描述符有數(shù)據(jù)到來(lái)的時(shí)候通知你,并且,如果一直沒(méi)有數(shù)據(jù)來(lái),那么希望20s之后能通知你。 你改怎么做?

進(jìn)一步假設(shè),你希望關(guān)注 100 個(gè)fd, 并希望這個(gè)一百個(gè)fd,從他們加入監(jiān)聽(tīng)隊(duì)列的時(shí)候開(kāi)始計(jì)算,20s后通知你超時(shí)

如果使用這個(gè)epoll_wait的話,是不是要自己記住所有fd的剩余超時(shí)時(shí)間呢?

libevent就解決了這個(gè)困擾。

配置和調(diào)試

net.ipv4.tcp_syncookies = 1 表示開(kāi)啟SYN Cookies。當(dāng)出現(xiàn)SYN等待隊(duì)列溢出時(shí),啟用cookies來(lái)處理,可防范少量SYN攻擊,默認(rèn)為0,表示關(guān)閉;

net.ipv4.tcp_tw_reuse = 1 表示開(kāi)啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉;

net.ipv4.tcp_tw_recycle = 1 表示開(kāi)啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉。

net.ipv4.tcp_fin_timeout 修改系默認(rèn)的 TIMEOUT 時(shí)間

優(yōu)化time_wait為啥要開(kāi)syncookie呢? syncookie可以繞過(guò)seq queue的限制,跟優(yōu)化time_wait沒(méi)有關(guān)系

另外得打開(kāi)timestamps,才能讓reuse、recycle生效。

修改fin_timeout,如何調(diào)?調(diào)大調(diào)小呢?

//測(cè)試數(shù)據(jù)

單線程,峰值處理鏈接,貌似目前測(cè)試條件可以測(cè)試到2.9K

CONNECT: 2.9K/s

QPS : 27.6萬(wàn)

5萬(wàn)的鏈接。

QPS : 1.9萬(wàn)

客戶端、服務(wù)端支持多少連接

客戶端

現(xiàn)在我們終于可以得出更為正確的結(jié)論了,對(duì)于有1個(gè)Ip的客戶端來(lái)說(shuō),受限于ip_local_port_range參數(shù),也受限于65535。但單Linux可以配置多個(gè)ip,有幾個(gè)ip,最大理論值就翻幾倍

多張網(wǎng)卡不是必須的。即使只有一張網(wǎng)卡,也可以配置多ip。k8s就是這么干的,在k8s里,一臺(tái)物理機(jī)上可以部署多個(gè)pod。但每一個(gè)pod都會(huì)被分配一個(gè)獨(dú)立的ip,所以完全不用擔(dān)心物理機(jī)上部署了過(guò)多的pod而影響你用的pod里的TCP連接數(shù)量。在ip給你的那一刻,你的pod就和其它應(yīng)用隔離開(kāi)了。

服務(wù)器端

一條TCP連接如果不發(fā)送數(shù)據(jù)的話,消耗內(nèi)存是3.3K左右。如果有數(shù)據(jù)發(fā)送,需要為每條TCP分配發(fā)送緩存區(qū),大小受你的參數(shù)net.ipv4.tcp_wmem配置影響,默認(rèn)情況下最小是4K。如果發(fā)送結(jié)束,緩存區(qū)消耗的內(nèi)存會(huì)被回收。

假設(shè)你只保持連接不發(fā)送數(shù)據(jù),那么你服務(wù)器可以建立的連接最大數(shù)量 = 你的內(nèi)存/3.3K。假如是4GB的內(nèi)存,那么大約可接受的TCP連接數(shù)量是100萬(wàn)左右。

這個(gè)例子里,我們考慮的前提是在一個(gè)進(jìn)程下hold所有的服務(wù)器端連接。而在實(shí)際中的項(xiàng)目里,為了收發(fā)數(shù)據(jù)方便,很多網(wǎng)絡(luò)IO模型還會(huì)為TCP連接再創(chuàng)建一個(gè)線程或協(xié)程。拿最輕量的golang來(lái)說(shuō),一個(gè)協(xié)程棧也需要2KB的內(nèi)存開(kāi)銷。

結(jié)論

TCP連接的客戶端機(jī):每一個(gè)ip可建立的TCP連接理論受限于ip_local_port_range參數(shù),也受限于65535。但可以通過(guò)配置多ip的方式來(lái)加大自己的建立連接的能力。

TCP連接的服務(wù)器機(jī):每一個(gè)監(jiān)聽(tīng)的端口雖然理論值很大,但這個(gè)數(shù)字沒(méi)有實(shí)際意義。最大并發(fā)數(shù)取決你的內(nèi)存大小,每一條靜止?fàn)顟B(tài)的TCP連接大約需要吃3.3K的內(nèi)存。

select、poll、epoll之間的區(qū)別(搜狗面試)

(1)select==>時(shí)間復(fù)雜度O(n)

它僅僅知道了,有I/O事件發(fā)生了,卻并不知道是哪那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能無(wú)差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫(xiě)入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。所以select具有O(n)的無(wú)差別輪詢復(fù)雜度,同時(shí)處理的流越多,無(wú)差別輪詢時(shí)間就越長(zhǎng)。

(2)poll==>時(shí)間復(fù)雜度O(n)

poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài), 但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的.

(3)epoll==>時(shí)間復(fù)雜度O(1)

epoll可以理解為event poll,不同于忙輪詢和無(wú)差別輪詢,epoll會(huì)把哪個(gè)流發(fā)生了怎樣的I/O事件通知我們。所以我們說(shuō)epoll實(shí)際上是事件驅(qū)動(dòng)(每個(gè)事件關(guān)聯(lián)上fd)的,此時(shí)我們對(duì)這些流的操作都是有意義的。(復(fù)雜度降低到了O(1))

select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫(xiě)就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫(xiě)操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫(xiě)事件就緒后自己負(fù)責(zé)進(jìn)行讀寫(xiě),也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的,而異步I/O則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫(xiě),異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

epoll跟select都能提供多路I/O復(fù)用的解決方案。在現(xiàn)在的Linux內(nèi)核里有都能夠支持,其中epoll是Linux所特有,而select則應(yīng)該是POSIX所規(guī)定,一般操作系統(tǒng)均有實(shí)現(xiàn)

select:

select本質(zhì)上是通過(guò)設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行下一步處理。這樣所帶來(lái)的缺點(diǎn)是:

1、 單個(gè)進(jìn)程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽(tīng)端口的大小有限。

一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機(jī)默認(rèn)是1024個(gè)。64位機(jī)默認(rèn)是2048.

2、 對(duì)socket進(jìn)行掃描時(shí)是線性掃描,即采用輪詢的方法,效率較低:

當(dāng)套接字比較多的時(shí)候,每次select()都要通過(guò)遍歷FD_SETSIZE個(gè)Socket來(lái)完成調(diào)度,不管哪個(gè)Socket是活躍的,都遍歷一遍。這會(huì)浪費(fèi)很多CPU時(shí)間。如果能給套接字注冊(cè)某個(gè)回調(diào)函數(shù),當(dāng)他們活躍時(shí),自動(dòng)完成相關(guān)操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護(hù)一個(gè)用來(lái)存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開(kāi)銷大

poll:

poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷,如果遍歷完所有fd后沒(méi)有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動(dòng)超時(shí),被喚醒后它又要再次遍歷fd。這個(gè)過(guò)程經(jīng)歷了多次無(wú)謂的遍歷。

它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的,但是同樣有一個(gè)缺點(diǎn):

1、大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義。

2、poll還有一個(gè)特點(diǎn)是“水平觸發(fā)”,如果報(bào)告了fd后,沒(méi)有被處理,那么下次poll時(shí)會(huì)再次報(bào)告該fd。

epoll:

epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認(rèn)的模式,ET是“高速”模式。LT模式下,只要這個(gè)fd還有數(shù)據(jù)可讀,每次 epoll_wait都會(huì)返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發(fā))模式中,它只會(huì)提示一次,直到下次再有數(shù)據(jù)流入之前都不會(huì)再提示了,無(wú) 論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個(gè)fd的時(shí)候一定要把它的buffer讀光,也就是說(shuō)一直讀到read的返回值小于請(qǐng)求值,或者 遇到EAGAIN錯(cuò)誤。還有一個(gè)特點(diǎn)是,epoll使用“事件”的就緒通知方式,通過(guò)epoll_ctl注冊(cè)fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來(lái)激活該fd,epoll_wait便可以收到通知。

epoll為什么要有EPOLLET觸發(fā)模式?

如果采用EPOLLLT模式的話,系統(tǒng)中一旦有大量你不需要讀寫(xiě)的就緒文件描述符,它們每次調(diào)用epoll_wait都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發(fā)模式的話,當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你?。?!這種模式比水平觸發(fā)效率高,系統(tǒng)不會(huì)充斥大量你不關(guān)心的就緒文件描述符

epoll的優(yōu)點(diǎn):

1、沒(méi)有最大并發(fā)連接的限制,能打開(kāi)的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽(tīng)約10萬(wàn)個(gè)端口);

2、效率提升,不是輪詢的方式,不會(huì)隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會(huì)調(diào)用callback函數(shù);

即Epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接,而跟連接總數(shù)無(wú)關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll。

3、 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少?gòu)?fù)制開(kāi)銷。

select、poll、epoll 區(qū)別總結(jié):

1、支持一個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)

select

單個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個(gè)整數(shù)的大?。ㄔ?2位的機(jī)器上,大小就是3232,同理64位機(jī)器上FD_SETSIZE為3264),當(dāng)然我們可以對(duì)進(jìn)行修改,然后重新編譯內(nèi)核,但是性能可能會(huì)受到影響,這需要進(jìn)一步的測(cè)試。

poll

poll本質(zhì)上和select沒(méi)有區(qū)別,但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的

epoll

雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機(jī)器上可以打開(kāi)10萬(wàn)左右的連接,2G內(nèi)存的機(jī)器可以打開(kāi)20萬(wàn)左右的連接

2、FD劇增后帶來(lái)的IO效率問(wèn)題

select

因?yàn)槊看握{(diào)用時(shí)都會(huì)對(duì)連接進(jìn)行線性遍歷,所以隨著FD的增加會(huì)造成遍歷速度慢的“線性下降性能問(wèn)題”。

poll

同上

epoll

因?yàn)閑poll內(nèi)核中實(shí)現(xiàn)是根據(jù)每個(gè)fd上的callback函數(shù)來(lái)實(shí)現(xiàn)的,只有活躍的socket才會(huì)主動(dòng)調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒(méi)有前面兩者的線性下降的性能問(wèn)題,但是所有socket都很活躍的情況下,可能會(huì)有性能問(wèn)題。

3、 消息傳遞方式

select

內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動(dòng)作

poll

同上

epoll

epoll通過(guò)內(nèi)核和用戶空間共享一塊內(nèi)存來(lái)實(shí)現(xiàn)的。

總結(jié):

綜上,在選擇select,poll,epoll時(shí)要根據(jù)具體的使用場(chǎng)合以及這三種方式的自身特點(diǎn)。

1、表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機(jī)制需要很多函數(shù)回調(diào)。

2、select低效是因?yàn)槊看嗡夹枰喸?。但低效也是相?duì)的,視情況而定,也可通過(guò)良好的設(shè)計(jì)改善

今天對(duì)這三種IO多路復(fù)用進(jìn)行對(duì)比,參考網(wǎng)上和書(shū)上面的資料,整理如下:

1、select實(shí)現(xiàn)

select的調(diào)用過(guò)程如下所示:

c088e39a-716d-11ee-939d-92fbcf53809c.jpg

(1)使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間

(2)注冊(cè)回調(diào)函數(shù)__pollwait

(3)遍歷所有fd,調(diào)用其對(duì)應(yīng)的poll方法(對(duì)于socket,這個(gè)poll方法是sock_poll,sock_poll根據(jù)情況會(huì)調(diào)用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實(shí)現(xiàn)就是__pollwait,也就是上面注冊(cè)的回調(diào)函數(shù)。

(5)__pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊(duì)列中,不同的設(shè)備有不同的等待隊(duì)列,對(duì)于tcp_poll來(lái)說(shuō),其等待隊(duì)列是sk->sk_sleep(注意把進(jìn)程掛到等待隊(duì)列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫(xiě)完文件數(shù)據(jù)(磁盤設(shè)備)后,會(huì)喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程,這時(shí)current便被喚醒了。

(6)poll方法返回時(shí)會(huì)返回一個(gè)描述讀寫(xiě)操作是否就緒的mask掩碼,根據(jù)這個(gè)mask掩碼給fd_set賦值。

(7)如果遍歷完所有的fd,還沒(méi)有返回一個(gè)可讀寫(xiě)的mask掩碼,則會(huì)調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠。當(dāng)設(shè)備驅(qū)動(dòng)發(fā)生自身資源可讀寫(xiě)后,會(huì)喚醒其等待隊(duì)列上睡眠的進(jìn)程。如果超過(guò)一定的超時(shí)時(shí)間(schedule_timeout指定),還是沒(méi)人喚醒,則調(diào)用select的進(jìn)程會(huì)重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒(méi)有就緒的fd。

(8)把fd_set從內(nèi)核空間拷貝到用戶空間。

總結(jié):

select的幾大缺點(diǎn):

(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷在fd很多時(shí)會(huì)很大

(2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,這個(gè)開(kāi)銷在fd很多時(shí)也很大

(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024

2 poll實(shí)現(xiàn)

poll的實(shí)現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),其他的都差不多,管理多個(gè)描述符也是進(jìn)行輪詢,根據(jù)描述符的狀態(tài)進(jìn)行處理,但是poll沒(méi)有最大文件描述符數(shù)量的限制。poll和select同樣存在一個(gè)缺點(diǎn)就是,包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開(kāi)銷隨著文件描述符數(shù)量的增加而線性增大。

3、epoll

epoll既然是對(duì)select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊(cè)要監(jiān)聽(tīng)的事件類型;epoll_wait則是等待事件的產(chǎn)生。

對(duì)于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過(guò)程中只會(huì)拷貝一次。

對(duì)于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒(méi)有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。

對(duì)于第三個(gè)缺點(diǎn),epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

總結(jié):

(1)select,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實(shí)也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時(shí),調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時(shí)候要遍歷整個(gè)fd集合,而epoll在“醒著”的時(shí)候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時(shí)間。這就是回調(diào)機(jī)制帶來(lái)的性能提升。

(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊(duì)列中掛一次,而epoll只要一次拷貝,而且把current往等待隊(duì)列上掛也只掛一次(在epoll_wait的開(kāi)始,注意這里的等待隊(duì)列并不是設(shè)備等待隊(duì)列,只是一個(gè)epoll內(nèi)部定義的等待隊(duì)列)。這也能節(jié)省不少的開(kāi)銷。

審核編輯:黃飛

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 服務(wù)器
    +關(guān)注

    關(guān)注

    12

    文章

    9321

    瀏覽量

    86103
  • 路由器
    +關(guān)注

    關(guān)注

    22

    文章

    3745

    瀏覽量

    114516
  • epoll
    +關(guān)注

    關(guān)注

    0

    文章

    28

    瀏覽量

    2985
  • select
    +關(guān)注

    關(guān)注

    0

    文章

    28

    瀏覽量

    3957

原文標(biāo)題:select、poll、epoll之間的區(qū)別(搜狗面試)

文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    epoll的使用

    API可以檢查多個(gè)文件描述符上的I/O就緒狀態(tài)。epoll API的主要優(yōu)點(diǎn)1.當(dāng)有大量的文件描述符需要檢查時(shí),epoll的性能延展性比select()和epoll(高很多)2.
    發(fā)表于 05-11 13:22

    我讀過(guò)的最好的epoll講解

    select以及epoll)處理甚至直接忽略。 為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個(gè)代理(一開(kāi)始有一位叫做select的代理,后來(lái)又有一位叫做poll的代理,不過(guò)兩者的本質(zhì)是一樣的)。
    發(fā)表于 05-12 15:30

    epoll使用方法與poll區(qū)別

    因?yàn)?b class='flag-5'>epoll的觸發(fā)機(jī)制是在內(nèi)核中直接完成整個(gè)功能 那個(gè)事件準(zhǔn)備就緒我就直接返回這個(gè)IO事件
    發(fā)表于 07-31 10:03

    揭示EPOLL一些原理性的東西

    事件交給其他對(duì)象(后文介紹的select以及epoll)處理甚至直接忽略。為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個(gè)代理(一開(kāi)始有一位叫做select的代理,后來(lái)又有一位叫做poll的代理,不
    發(fā)表于 08-24 16:32

    epollselect區(qū)別

     select,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫(xiě)就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫(xiě)操作。但select
    發(fā)表于 11-10 16:20 ?2.1w次閱讀
    <b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>的<b class='flag-5'>區(qū)別</b>

    關(guān)于Epoll,你應(yīng)該知道的那些細(xì)節(jié)

    Epoll,位于頭文件sys/epoll.h,是Linux系統(tǒng)上的I/O事件通知基礎(chǔ)設(shè)施。epoll API為L(zhǎng)inux系統(tǒng)專有,于內(nèi)核2.5.44中首次引入,glibc于2.3.2版本加入支持。其它提供類似的功能的系統(tǒng),包括F
    發(fā)表于 05-12 09:25 ?1218次閱讀

    poll&&epollepoll實(shí)現(xiàn)

    poll&&epollepoll實(shí)現(xiàn)
    發(fā)表于 05-14 14:34 ?2818次閱讀
    <b class='flag-5'>poll</b>&&<b class='flag-5'>epoll</b>之<b class='flag-5'>epoll</b>實(shí)現(xiàn)

    詳細(xì)解讀Linux內(nèi)核的poll機(jī)制

    對(duì)于系統(tǒng)調(diào)用pollselect,它們對(duì)應(yīng)的內(nèi)核函數(shù)都是sys_poll。分析sys_poll,即可理解poll機(jī)制。
    發(fā)表于 05-14 16:22 ?4096次閱讀
    詳細(xì)解讀Linux內(nèi)核的<b class='flag-5'>poll</b>機(jī)制

    Linux內(nèi)核中select, pollepoll區(qū)別

    先說(shuō)poll,pollselect為大部分Unix/Linux程序員所熟悉,這倆個(gè)東西原理類似,性能上也不存在明顯差異,但select對(duì)所監(jiān)控的文件描述符數(shù)量有限制,所以這里選用
    發(fā)表于 05-14 16:24 ?1738次閱讀

    Linux中epoll IO多路復(fù)用機(jī)制

    epoll 是Linux內(nèi)核中的一種可擴(kuò)展IO事件處理機(jī)制,最早在 Linux 2.5.44內(nèi)核中引入,可被用于代替POSIX selectpoll 系統(tǒng)調(diào)用,并且在具有大量應(yīng)用程序請(qǐng)求時(shí)能夠
    發(fā)表于 05-16 16:07 ?721次閱讀
    Linux中<b class='flag-5'>epoll</b> IO多路復(fù)用機(jī)制

    epoll LT和ET方式下的讀寫(xiě)差別

    epoll接口是為解決Linux內(nèi)核處理大量文件描述符而提出的方案。該接口屬于Linux下多路I/O復(fù)用接口中select/poll的增強(qiáng)。
    的頭像 發(fā)表于 07-07 10:34 ?2220次閱讀

    epollselect使用區(qū)別

    epollselect 相比于select,epoll最大的好處在于它不會(huì)隨著監(jiān)聽(tīng)fd數(shù)目的增長(zhǎng)而降低效率。因?yàn)樵趦?nèi)核中的select實(shí)
    的頭像 發(fā)表于 11-09 14:14 ?1190次閱讀
    <b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>使用<b class='flag-5'>區(qū)別</b>

    epoll底層如何使用紅黑樹(shù)

    epollpoll的一個(gè)很大的區(qū)別在于,poll每次調(diào)用時(shí)都會(huì)存在一個(gè)將pollfd結(jié)構(gòu)體數(shù)組中的每個(gè)結(jié)構(gòu)體元素從用戶態(tài)向內(nèi)核態(tài)中的一個(gè)鏈表節(jié)點(diǎn)拷貝的過(guò)程,而內(nèi)核中的這個(gè)鏈表并不會(huì)一
    的頭像 發(fā)表于 11-10 15:13 ?764次閱讀
    <b class='flag-5'>epoll</b>底層如何使用紅黑樹(shù)

    Epoll封裝類實(shí)現(xiàn)

    關(guān)于epoll的原理,以及和poll、select、IOCP之間的比較,網(wǎng)上的資料很多,這些都屬于I/O復(fù)用的實(shí)現(xiàn)方法,即可以同時(shí)監(jiān)聽(tīng)發(fā)生在多個(gè)I/O端口(socket套接字描述符或文件描述符
    的頭像 發(fā)表于 11-13 11:54 ?554次閱讀

    Linux--IO多路復(fù)用(select,pollepoll)

    ,常用的系統(tǒng)調(diào)用包括select()、poll()和epoll()。這些機(jī)制允許程序監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(通常是讀就緒或?qū)懢途w),程序就會(huì)被通知進(jìn)行相應(yīng)的讀寫(xiě)操作。這個(gè)過(guò)程通常涉及兩個(gè)階段
    的頭像 發(fā)表于 11-06 16:13 ?446次閱讀