精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Linux 五種 IO 模型和三種多路復用技術(shù)大詳解

系統(tǒng) Linux
select,poll,epoll都是IO多路復用機制,即可以監(jiān)視多個描述符,一旦某個描述符就緒(讀或?qū)懢途w),能夠通知程序進行相應讀寫操作。

作者 | symen

在Linux系統(tǒng)中,實際上所有的I/O設(shè)備都被抽象為文件這個概念,一切皆文件(Everything is File)。無論是磁盤、網(wǎng)絡(luò)數(shù)據(jù)、終端,還是進程間通信工具(如:管道pipe)等都被抽象為文件的概念。 這種設(shè)計使得 I/O 操作可以通過統(tǒng)一的文件描述符(File Descriptor, FD)來管理。 在了解多路復用select、poll、epoll實現(xiàn)之前,我們先簡單回憶復習以下兩個概念。

一、什么是多路復用

多路: 是指多個網(wǎng)絡(luò)連接(Socket)

復用: 是指通過一個線程同時監(jiān)控多個文件描述符的就緒狀態(tài)。這樣,程序可以高效地處理多個 I/O 事件,而不需要為每個連接創(chuàng)建單獨的線程,從而節(jié)省系統(tǒng)資源。

多路復用主要有三種技術(shù):select,poll,epoll。其中,epoll 是最新且性能最優(yōu)的實現(xiàn),能夠高效地處理大規(guī)模并發(fā)連接。

二、五種IO模型

[1]blockingIO - 阻塞IO
[2]nonblockingIO - 非阻塞IO
[3]signaldrivenIO - 信號驅(qū)動IO
[4]asynchronousIO - 異步IO
[5]IOmultiplexing - IO多路復用

1. 阻塞式I/O模型

在阻塞式 I/O 模型中,在I/O操作的兩個階段均會阻塞線程:

  • 數(shù)據(jù)等待階段:當進程或線程發(fā)起IO請求(如:調(diào)用 recvfrom 系統(tǒng)調(diào)用)時,它會一直阻塞,直到內(nèi)核確認數(shù)據(jù)已準備好(例:網(wǎng)卡接收數(shù)據(jù)、網(wǎng)絡(luò)數(shù)據(jù)到達內(nèi)核緩沖區(qū))。
  • 數(shù)據(jù)復制階段:內(nèi)核將數(shù)據(jù)從內(nèi)核空間復制到用戶空間時,線程/進程仍處于阻塞狀態(tài)。此過程線程/進程在等待I/O完成期間無法執(zhí)行其他任務(wù)(被掛起),CPU資源可能閑置。

主要特點:

  • 阻塞掛起: 進程/線程在等待數(shù)據(jù)時會被掛起,不占用 CPU 資源。
  • 及時響應: 每個操作都能得到及時處理,適合對實時性要求較高的場景。
  • 實現(xiàn)簡單: 開發(fā)難度低,邏輯直觀,代碼按順序執(zhí)行,無需處理多線程或異步回調(diào)的復雜性。
  • 適用場景: 阻塞式 I/O 模型適合并發(fā)量較小、對實時性要求較高的應用。但在高并發(fā)場景中,其系統(tǒng)開銷和性能限制使其不再適用。
  • 系統(tǒng)開銷大:由于每個請求都會阻塞進程/線程,因此需要為每個請求分配獨立的進程或線程來處理。在高并發(fā)場景下,這種模型會消耗大量系統(tǒng)資源(如內(nèi)存和上下文切換開銷),導致性能瓶頸。

2. 非阻塞式I/O模型

在非阻塞式 I/O 模型中,當進程發(fā)起 I/O 系統(tǒng)調(diào)用(如 recvfrom)時:

  • 如果內(nèi)核緩沖區(qū)沒有數(shù)據(jù),內(nèi)核會立即返回一個錯誤(如 EWOULDBLOCK 或 EAGAIN),而不會阻塞進程。
  • 如果內(nèi)核緩沖區(qū)有數(shù)據(jù),內(nèi)核會將數(shù)據(jù)復制到用戶空間并返回成功。

主要特點:

  • 非阻塞: 進程不會被掛起,無論是否有數(shù)據(jù)都會立即返回。
  • 輪詢機制: 進程需要不斷發(fā)起系統(tǒng)調(diào)用(輪詢)來檢查數(shù)據(jù)是否就緒,這會消耗大量 CPU 資源。
  • 實現(xiàn)難度較低: 相比阻塞式 I/O,開發(fā)復雜度稍高,但仍屬于較簡單的模型。
  • 實時性差: 輪詢機制無法保證及時響應數(shù)據(jù)到達事件,可能導致延遲。
  • 適用場景: 適合并發(fā)量較小、且對實時性要求不高的網(wǎng)絡(luò)應用開發(fā)。由于其 CPU 開銷較大,通常不適用于高并發(fā)或高性能場景。

3. 信號驅(qū)動IO

在信號驅(qū)動 I/O 模型中,進程發(fā)起一個 I/O 操作時,會向內(nèi)核注冊一個信號處理函數(shù)(如 SIGIO),然后立即返回,不會被阻塞。當內(nèi)核數(shù)據(jù)就緒時,會向進程發(fā)送一個信號,進程在信號處理函數(shù)中調(diào)用 I/O 操作(如 recvfrom)讀取數(shù)據(jù)。

主要特點:

  • 非阻塞: 進程在等待數(shù)據(jù)時不會被阻塞,可以繼續(xù)執(zhí)行其他任務(wù)。
  • 回調(diào)機制: 通過信號通知的方式實現(xiàn)異步事件處理,數(shù)據(jù)就緒時內(nèi)核主動通知進程。
  • 實現(xiàn)難度大: 信號處理函數(shù)的編寫和調(diào)試較為復雜,開發(fā)難度較高。
  • 信號處理復雜性: 信號處理函數(shù)需要處理異步事件,可能引入競態(tài)條件和不可預測的行為。

適用場景有限: 適合對實時性要求較高、但并發(fā)量較小的網(wǎng)絡(luò)應用開發(fā)。由于其實現(xiàn)復雜性和潛在問題,通常不適用于高并發(fā)或高性能場景。

4. 異步IO

在異步 I/O 模型中,當進程發(fā)起一個 I/O 操作時,會立即返回,不會被阻塞,也不會立即返回結(jié)果。內(nèi)核會負責完成整個 I/O 操作(包括數(shù)據(jù)準備和復制到用戶空間),并在操作完成后通知進程。如果 I/O 操作成功,進程可以直接獲取到數(shù)據(jù)。

主要特點:

  • 完全非阻塞: 進程在發(fā)起 I/O 操作后不會被阻塞,可以繼續(xù)執(zhí)行其他任務(wù)。
  • Proactor 模式: 內(nèi)核負責完成 I/O 操作并通知進程,進程只需處理最終結(jié)果。
  • 高性能: 適合高并發(fā)、高性能場景,能夠充分利用系統(tǒng)資源。
  • 操作系統(tǒng)支持: 異步 I/O 需要操作系統(tǒng)的底層支持。在 Linux 中,異步 I/O 從 2.5 版本內(nèi)核開始引入,并在 2.6 版本中成為標準特性。
  • 實現(xiàn)難度大: 異步 I/O 的開發(fā)復雜度較高,需要處理回調(diào)、事件通知等機制。

適用場景: 異步 I/O 模型非常適合高性能、高并發(fā)的網(wǎng)絡(luò)應用開發(fā),如大規(guī)模 Web 服務(wù)器、數(shù)據(jù)庫系統(tǒng)等。

5. IO復用模型

大多數(shù)文件系統(tǒng)的默認IO操作都是緩存IO。在Linux的緩存IO機制中,操作系統(tǒng)會將IO的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存(page cache)。也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩存區(qū)拷貝到應用程序的地址空間中。這種做法的缺點就是,需要在應用程序地址空間和內(nèi)核進行多次拷貝,這些拷貝動作所帶來的CPU以及內(nèi)存開銷是非常大的。 至于為什么不能直接讓磁盤控制器把數(shù)據(jù)送到應用程序的地址空間中呢?最簡單的一個原因就是應用程序不能直接操作底層硬件。 總的來說,IO分兩階段: 1)數(shù)據(jù)準備階段 2)內(nèi)核空間復制回用戶進程緩沖區(qū)階段。如下圖:

三、I/O 多路復用之select、poll、epoll詳解

目前支持I/O多路復用的系統(tǒng)調(diào)用有select,pselect,poll,epoll。與多進程和多線程技術(shù)相比,I/O多路復用技術(shù)的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不必創(chuàng)建進程/線程,也不必維護這些進程/線程,從而大大減小了系統(tǒng)的開銷。 I/O多路復用就是通過一種機制,一個進程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現(xiàn)會負責把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

1. select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • readfds:內(nèi)核檢測該集合中的IO是否可讀。如果想讓內(nèi)核幫忙檢測某個IO是否可讀,需要手動把文件描述符加入該集合。
  • writefds:內(nèi)核檢測該集合中的IO是否可寫。同readfds,需要手動把文件描述符加入該集合。
  • exceptfds:內(nèi)核檢測該集合中的IO是否異常。同readfds,需要手動把文件描述符加入該集合。
  • nfds:以上三個集合中最大的文件描述符數(shù)值 + 1,例如集合是{0,1,5,10},那么 maxfd 就是 11
  • timeout:用戶線程調(diào)用select的超時時長。
  • 設(shè)置成NULL,表示如果沒有 I/O 事件發(fā)生,則 select 一直等待下去。
  • 設(shè)置為非0的值,這個表示等待固定的一段時間后從 select 阻塞調(diào)用中返回。
  • 設(shè)置成 0,表示根本不等待,檢測完畢立即返回。

函數(shù)返回值:

  • 大于0:成功,返回集合中已就緒的IO總個數(shù)
  • 等于-1:調(diào)用失敗
  • 等于0:沒有就緒的IO

從上述的select函數(shù)聲明可以看出,fd_set本質(zhì)是一個數(shù)組,為了方便我們操作該數(shù)組,操作系統(tǒng)提供了以下函數(shù):

// 將文件描述符fd從set集合中刪除 
void FD_CLR(int fd, fd_set *set); 

// 判斷文件描述符fd是否在set集合中 
int  FD_ISSET(int fd, fd_set *set); 

// 將文件描述符fd添加到set集合中 
void FD_SET(int fd, fd_set *set); 

// 將set集合中, 所有文件描述符對應的標志位設(shè)置為0
void FD_ZERO(fd_set *set);

select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds,當用戶process調(diào)用select的時候,select會將需要監(jiān)控的readfds集合拷貝到內(nèi)核空間(假設(shè)監(jiān)控的僅僅是socket可讀),然后遍歷自己監(jiān)控的skb(SocketBuffer),挨個調(diào)用skb的poll邏輯以便檢查該socket是否有可讀事件,遍歷完所有的skb后,如果沒有任何一個socket可讀,那么select會調(diào)用schedule_timeout進入schedule循環(huán),使得process進入睡眠。如果在timeout時間內(nèi)某個socket上有數(shù)據(jù)可讀了,或者等待timeout了,則調(diào)用select的process會被喚醒,接下來select就是遍歷監(jiān)控的集合,挨個收集可讀事件并返回給用戶了,相應的偽碼如下:

int select(
    int nfds,
    fd_set *readfds,
    fd_set *writefds,
    fd_set *exceptfds,
    struct timeval *timeout);
// nfds:監(jiān)控的文件描述符集里最大文件描述符加1
// readfds:監(jiān)控有讀數(shù)據(jù)到達文件描述符集合,傳入傳出參數(shù)
// writefds:監(jiān)控寫數(shù)據(jù)到達文件描述符集合,傳入傳出參數(shù)
// exceptfds:監(jiān)控異常發(fā)生達文件描述符集合, 傳入傳出參數(shù)
// timeout:定時阻塞監(jiān)控時間,3種情況
//  1.NULL,永遠等下去
//  2.設(shè)置timeval,等待固定時間
//  3.設(shè)置timeval里時間均為0,檢查描述字后立即返回,輪詢

/* 
* select服務(wù)端偽碼
* 首先一個線程不斷接受客戶端連接,并把socket文件描述符放到一個list里。
*/
while(1) {
  connfd = accept(listenfd);
  fcntl(connfd, F_SETFL, O_NONBLOCK);
  fdlist.add(connfd);
}
/*
* select函數(shù)還是返回剛剛提交的list,應用程序依然list所有的fd,只不過操作系統(tǒng)會將準備就緒的文件描述符做上標識,
* 用戶層將不會再有無意義的系統(tǒng)調(diào)用開銷。
*/
struct timeval timeout;
int max = 0;  // 用于記錄最大的fd,在輪詢中時刻更新即可
// 初始化比特位
FD_ZERO(&read_fd);
while (1) {
    // 阻塞獲取 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
    nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
    // 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生
    for (int i = 0; i <= max && nfds; ++i) {
        // 只讀已就緒的文件描述符,不用過多遍歷
        if (i == listenfd) {
            // 這里處理accept事件
            FD_SET(i, &read_fd);//將客戶端socket加入到集合中
        }
        if (FD_ISSET(i, &read_fd)) {
            // 這里處理read事件
        }
    }
}

下面的動圖能更直觀的讓我們了解select:

通過上面的select邏輯過程分析,相信大家都意識到,select存在三個問題:

(1) 每次調(diào)用select,都需要把被監(jiān)控的fds集合從用戶態(tài)空間拷貝到內(nèi)核態(tài)空間,高并發(fā)場景下這樣的拷貝會使得消耗的資源是很大的。

(2) 能監(jiān)聽端口的數(shù)量有限,單個進程所能打開的最大連接數(shù)由FD_SETSIZE宏定義,監(jiān)聽上限就等于fds_bits位數(shù)組中所有元素的二進制位總數(shù),其大小是32個整數(shù)的大小(在32位的機器上,大小就是3232,同理64位機器上為3264),當然我們可以對宏FD_SETSIZE進行修改,然后重新編譯內(nèi)核,但是性能可能會受到影響,一般該數(shù)和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機默認1024個,64位默認2048。

(3) 被監(jiān)控的fds集合中,只要有一個有數(shù)據(jù)可讀,整個socket集合就會被遍歷一次調(diào)用sk的poll函數(shù)收集可讀事件:由于當初的需求是樸素,僅僅關(guān)心是否有數(shù)據(jù)可讀這樣一個事件,當事件通知來的時候,由于數(shù)據(jù)的到來是異步的,我們不知道事件來的時候,有多少個被監(jiān)控的socket有數(shù)據(jù)可讀了,于是,只能挨個遍歷每個socket來收集可讀事件了。

2. poll

poll 的實現(xiàn)與 select 非常相似,都是通過監(jiān)視多個文件描述符(fd)來實現(xiàn) I/O 多路復用。兩者的主要區(qū)別在于描述 fd 集合的方式:select 使用 fd_set 結(jié)構(gòu),而 poll 使用 pollfd 結(jié)構(gòu)。select 的 fd_set 結(jié)構(gòu)限制了 fd 集合的大小(通常為 1024),而 poll 使用 pollfd 結(jié)構(gòu),理論上可以支持更多的 fd,解決了 select 的問題 (2)。

與 select 類似,poll 也存在性能瓶頸。當監(jiān)視的 fd 數(shù)量較多時,poll 需要將整個 pollfd 數(shù)組在用戶態(tài)和內(nèi)核態(tài)之間復制,無論這些 fd 是否就緒。這種復制的開銷會隨著 fd 數(shù)量的增加而線性增長,導致性能下降。

poll 適合需要監(jiān)視較多 fd 的場景,但在高并發(fā)或 fd 數(shù)量非常大的情況下,性能仍然不如 epoll。

解決 fd 數(shù)量限制問題:

struct pollfd {
   int fd;           /*文件描述符*/
   short events;     /*監(jiān)控的事件*/
   short revents;    /*監(jiān)控事件中滿足條件返回的事件*/
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);

函數(shù)參數(shù):

  • fds:struct pollfd類型的數(shù)組, 存儲了待檢測的文件描述符,struct pollfd有三個成員:
  • fd:委托內(nèi)核檢測的文件描述符
  • events:委托內(nèi)核檢測的fd事件(輸入、輸出、錯誤),每一個事件有多個取值
  • revents:這是一個傳出參數(shù),數(shù)據(jù)由內(nèi)核寫入,存儲內(nèi)核檢測之后的結(jié)果
  • nfds:描述的是數(shù)組 fds 的大小
  • timeout: 指定poll函數(shù)的阻塞時長
  • -1:一直阻塞,直到檢測的集合中有就緒的IO事件,然后解除阻塞函數(shù)返回
  • 0:不阻塞,不管檢測集合中有沒有已就緒的IO事件,函數(shù)馬上返回
  • 大于0:表示 poll 調(diào)用方等待指定的毫秒數(shù)后返回

函數(shù)返回值:

  • -1:失敗
  • 大于0:表示檢測的集合中已就緒的文件描述符的總個數(shù)

下面是poll的函數(shù)原型,poll改變了fds集合的描述方式,使用了pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),使得poll支持的fds集合限制遠大于select的1024。poll雖然解決了fds集合大小1024的限制問題,從實現(xiàn)來看。很明顯它并沒優(yōu)化大量描述符數(shù)組被整體復制于用戶態(tài)和內(nèi)核態(tài)的地址空間之間,以及個別描述符就緒觸發(fā)整體描述符集合的遍歷的低效問題。poll隨著監(jiān)控的socket集合的增加性能線性下降,使得poll也并不適合用于大并發(fā)場景。

poll服務(wù)端實現(xiàn)偽碼:
struct pollfd fds[POLL_LEN];
unsigned int nfds=0;
fds[0].fd=server_sockfd;
fds[0].events=POLLIN|POLLPRI;
nfds++;
while {
    res=poll(fds,nfds,-1);
    if(fds[0].revents&(POLLIN|POLLPRI)) {
        //執(zhí)行accept并加入fds中,nfds++
        if(--res<=0) continue
    }
    //循環(huán)之后的fds
    if(fds[i].revents&(POLLIN|POLLERR )) {
        //讀操作或處理異常等
        if(--res<=0) continue
    }
}

3. epoll

在 Linux 網(wǎng)絡(luò)編程中,select 曾長期被用作事件觸發(fā)的機制。然而,隨著高并發(fā)場景的需求增加,select 的性能瓶頸逐漸顯現(xiàn)。為了解決這些問題,Linux 內(nèi)核引入了 epoll 機制。相比于 select,epoll 的最大優(yōu)勢在于其性能不會隨著監(jiān)聽的文件描述符(fd)數(shù)量的增加而顯著下降。如前面我們所說,在內(nèi)核中的select實現(xiàn)中,它是采用輪詢來處理的,輪詢的fd數(shù)目越多,自然耗時越多。并且,在linux/posix_types.h頭文件有這樣的聲明:

#define __FD_SETSIZE 1024 (select 最多只能同時監(jiān)聽 1024 個 fd(由 __FD_SETSIZE 定義)。雖然可以通過修改內(nèi)核頭文件并重新編譯內(nèi)核來擴大這一限制,但這并不能從根本上解決問題。) 而epoll 使用基于事件回調(diào)的機制,而不是輪詢。它只會關(guān)注活躍的 fd,因此性能不會隨著 fd 數(shù)量的增加而顯著下降。

epoll 的使用: epoll的接口非常簡單,一共就三個函數(shù):

  • epoll_create創(chuàng)建句柄:使用 epoll_create 創(chuàng)建一個 epoll 句柄。參數(shù) size 用于告訴內(nèi)核監(jiān)聽 fd 的數(shù)量(在較新的內(nèi)核中,size 參數(shù)已被忽略,但仍需大于 0),這個參數(shù)不同于select()中的第一個參數(shù),給出最大監(jiān)聽的fd+1的值。需要注意的是,當創(chuàng)建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導致fd被耗盡。
  • epoll_ctl管理連接:使用 epoll_ctl 向 epoll 對象中添加、修改或刪除要管理的 fd。
  • epoll_wait等待事件:使用 epoll_wait 等待其管理的 fd 上的 I/O 事件。

(1) epoll_create 函數(shù)

int epoll_create(int size);
  • 功能:該函數(shù)生成一個 epoll 專用的文件描述符。
  • 參數(shù)size: 用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大,參數(shù) size 并不是限制了 epoll 所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個建議。自從 linux 2.6.8 之后,size 參數(shù)是被忽略的,也就是說可以填只有大于 0 的任意值。
  • 返回值:如果成功,返回poll 專用的文件描述符,否者失敗,返回-1。

epoll_create的源碼實現(xiàn):

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    struct eventpoll *ep = NULL;

    //創(chuàng)建一個 eventpoll 對象
    error = ep_alloc(&ep);
}

//struct eventpoll 的定義
// file:fs/eventpoll.c
struct eventpoll {

    //sys_epoll_wait用到的等待隊列
    wait_queue_head_t wq;

    //接收就緒的描述符都會放到這里
    struct list_head rdllist;

    //每個epoll對象中都有一顆紅黑樹
    struct rb_root rbr;

    ......
}
static int ep_alloc(struct eventpoll **pep)
{
    struct eventpoll *ep;

    //申請 epollevent 內(nèi)存
    ep = kzalloc(sizeof(*ep), GFP_KERNEL);

    //初始化等待隊列頭
    init_waitqueue_head(&ep->wq);

    //初始化就緒列表
    INIT_LIST_HEAD(&ep->rdllist);

    //初始化紅黑樹指針
    ep->rbr = RB_ROOT;

    ......
}

其中eventpoll 這個結(jié)構(gòu)體中的幾個成員的含義如下:

  • wq: 等待隊列鏈表。軟中斷數(shù)據(jù)就緒的時候會通過 wq 來找到阻塞在 epoll 對象上的用戶進程。
  • rbr: 紅黑樹。為了支持對海量連接的高效查找、插入和刪除,eventpoll 內(nèi)部使用的就是紅黑樹。通過紅黑樹來管理用戶主進程accept添加進來的所有 socket 連接。
  • rdllist: 就緒的描述符鏈表。當有連接就緒的時候,內(nèi)核會把就緒的連接放到 rdllist 鏈表里。這樣應用進程只需要判斷鏈表就能找出就緒進程,而不用去遍歷紅黑樹的所有節(jié)點了。

(2) epoll_ctl 函數(shù)

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:epoll 的事件注冊函數(shù),它不同于 select() 是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型。
  • 參數(shù)epfd: epoll 專用的文件描述符,epoll_create()的返回值
  • 參數(shù)op: 表示動作,用三個宏來表示:
1. EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中;
2. EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
3. EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;

- 參數(shù)fd: 需要監(jiān)聽的文件描述符
- 參數(shù)event: 告訴內(nèi)核要監(jiān)聽什么事件,struct epoll_event 結(jié)構(gòu)如:
- events 可以是以下幾個宏的集合:
- EPOLLIN :表示對應的文件描述符可以讀(包括對端 SOCKET 正常關(guān)閉);
- EPOLLOUT:表示對應的文件描述符可以寫;
- EPOLLPRI:表示對應的文件描述符有緊急的數(shù)據(jù)可讀(這里應該表示有帶外數(shù)據(jù)到來);
- EPOLLERR:表示對應的文件描述符發(fā)生錯誤;
- EPOLLHUP:表示對應的文件描述符被掛斷;
- EPOLLET :將 EPOLL 設(shè)為邊緣觸發(fā)(Edge Trigger)模式,這是相對于水平觸發(fā)(Level Trigger)來說的。
- EPOLLONESHOT:只監(jiān)聽一次事件,當監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個 socket 的話,需要再次把這個 socket 加入到 EPOLL 隊列里
- 返回值:0表示成功,-1表示失敗。

(3) epoll_wait函數(shù)

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 功能:等待事件的產(chǎn)生,收集在 epoll 監(jiān)控的事件中已經(jīng)發(fā)送的事件,類似于 select() 調(diào)用。
  • 參數(shù)epfd: epoll 專用的文件描述符,epoll_create()的返回值
  • 參數(shù)events: 分配好的 epoll_event 結(jié)構(gòu)體數(shù)組,epoll 將會把發(fā)生的事件賦值到events 數(shù)組中(events 不可以是空指針,內(nèi)核只負責把數(shù)據(jù)復制到這個 events 數(shù)組中,不會去幫助我們在用戶態(tài)中分配內(nèi)存)。
  • 參數(shù)maxevents: maxevents 告之內(nèi)核這個 events 有多少個 。
  • 參數(shù)timeout: 超時時間,單位為毫秒,為 -1 時,函數(shù)為阻塞。

返回值:

  • 如果成功,表示返回需要處理的事件數(shù)目
  • 如果返回0,表示已超時
  • 如果返回-1,表示失敗
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include<iostream>
const int MAX_EVENT_NUMBER = 10000; //最大事件數(shù)
// 設(shè)置句柄非阻塞
int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

int main(){

    // 創(chuàng)建套接字
    int nRet=0;
    int m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if(m_listenfd<0)
    {
        printf("fail to socket!");
        return -1;
    }
    // 
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons(6666);

    int flag = 1;
    // 設(shè)置ip可重用
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    // 綁定端口號
    int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    if(ret<0)
    {
        printf("fail to bind!,errno :%d",errno);
        return ret;
    }

    // 監(jiān)聽連接fd
    ret = listen(m_listenfd, 200);
    if(ret<0)
    {
        printf("fail to listen!,errno :%d",errno);
        return ret;
    }

    // 初始化紅黑樹和事件鏈表結(jié)構(gòu)rdlist結(jié)構(gòu)
    epoll_event events[MAX_EVENT_NUMBER];
    // 創(chuàng)建epoll實例
    int m_epollfd = epoll_create(5);
    if(m_epollfd==-1)
    {
        printf("fail to epoll create!");
        return m_epollfd;
    }


    // 創(chuàng)建節(jié)點結(jié)構(gòu)體將監(jiān)聽連接句柄
    epoll_event event;
    event.data.fd = m_listenfd;
    //設(shè)置該句柄為邊緣觸發(fā)(數(shù)據(jù)沒處理完后續(xù)不會再觸發(fā)事件,水平觸發(fā)是不管數(shù)據(jù)有沒有觸發(fā)都返回事件),
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    // 添加監(jiān)聽連接句柄作為初始節(jié)點進入紅黑樹結(jié)構(gòu)中,該節(jié)點后續(xù)處理連接的句柄
    epoll_ctl(m_epollfd, EPOLL_CTL_ADD, m_listenfd, &event);

    //進入服務(wù)器循環(huán)
    while(1)
    {
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if (number < 0 && errno != EINTR)
        {
            printf( "epoll failure");
            break;
        }
        for (int i = 0; i < number; i++)
        {
            int sockfd = events[i].data.fd;
            // 屬于處理新到的客戶連接
            if (sockfd == m_listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
                if (connfd < 0)
                {
                    printf("errno is:%d accept error", errno);
                    return false;
                }
                epoll_event event;
                event.data.fd = connfd;
                //設(shè)置該句柄為邊緣觸發(fā)(數(shù)據(jù)沒處理完后續(xù)不會再觸發(fā)事件,水平觸發(fā)是不管數(shù)據(jù)有沒有觸發(fā)都返回事件),
                event.events = EPOLLIN | EPOLLRDHUP;
                // 添加監(jiān)聽連接句柄作為初始節(jié)點進入紅黑樹結(jié)構(gòu)中,該節(jié)點后續(xù)處理連接的句柄
                epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, &event);
                setnonblocking(connfd);
            }
            else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
            {
                //服務(wù)器端關(guān)閉連接,
                epoll_ctl(m_epollfd, EPOLL_CTL_DEL, sockfd, 0);
                close(sockfd);
            }
            //處理客戶連接上接收到的數(shù)據(jù)
            else if (events[i].events & EPOLLIN)
            {
                char buf[1024]={0};
                read(sockfd,buf,1024);
                printf("from client :%s");

                // 將事件設(shè)置為寫事件返回數(shù)據(jù)給客戶端
                events[i].data.fd = sockfd;
                events[i].events = EPOLLOUT | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
                epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
            }
            else if (events[i].events & EPOLLOUT)
            {
                std::string response = "server response \n";
                write(sockfd,response.c_str(),response.length());

                // 將事件設(shè)置為讀事件,繼續(xù)監(jiān)聽客戶端
                events[i].data.fd = sockfd;
                events[i].events = EPOLLIN | EPOLLRDHUP;
                epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
            }
            //else if 可以加管道,unix套接字等等數(shù)據(jù)
        }
    }


}

如下圖,可以幫助我們理解的更加絲滑(/手動狗頭):

4. epoll的邊緣觸發(fā)與水平觸發(fā)

(1) 水平觸發(fā)(LT)

關(guān)注點是數(shù)據(jù)是否有無,只要讀緩沖區(qū)不為空,寫緩沖區(qū)不滿,那么epoll_wait就會一直返回就緒,水平觸發(fā)是epoll的默認工作方式。適合對事件處理邏輯要求不高的場景。

(2) 邊緣觸發(fā)(ET)

關(guān)注點是數(shù)據(jù)的變化。只有當緩沖區(qū)狀態(tài)發(fā)生變化時(例如從空變?yōu)榉强眨驈姆强兆優(yōu)榭眨琫poll_wait 才會返回就緒狀態(tài)。這里的數(shù)據(jù)變化并不單純指緩沖區(qū)從有數(shù)據(jù)變?yōu)闆]有數(shù)據(jù),或者從沒有數(shù)據(jù)變?yōu)橛袛?shù)據(jù),還包括了數(shù)據(jù)變多或者變少。即當buffer長度有變化時,就會觸發(fā)。 假設(shè)epoll被設(shè)置為了邊緣觸發(fā),當客戶端寫入了100個字符,由于緩沖區(qū)從0變?yōu)榱?00,于是服務(wù)端epoll_wait觸發(fā)一次就緒,服務(wù)端讀取了2個字節(jié)后不再讀取。這個時候再去調(diào)用epoll_wait會發(fā)現(xiàn)不會就緒,只有當客戶端再次寫入數(shù)據(jù)后,才會觸發(fā)就緒。 這就導致如果使用ET模式,那就必須保證要「一次性把數(shù)據(jù)讀取&寫入完」,否則會導致數(shù)據(jù)長期無法讀取/寫入。適合高性能場景,可以減少事件通知的次數(shù),提高效率。

4. epoll 為什么比select,poll更高效?

從上圖可以看出,epoll使用紅黑樹管理文件描述符,紅黑樹插入和刪除的都是時間復雜度 O(logN),不會隨著文件描述符數(shù)量增加而改變。 select、poll采用數(shù)組或者鏈表的形式管理文件描述符,那么在遍歷文件描述符時,時間復雜度會隨著文件描述的增加而增加,我們從以下幾點分析epoll的優(yōu)勢:

(1) 事件驅(qū)動機制(基于回調(diào),而非輪詢)

  • select 和 poll 的輪詢機制: select 和 poll 采用輪詢的方式檢查所有被監(jiān)視的文件描述符(fd),無論這些 fd 是否就緒。每次調(diào)用時,都需要將整個 fd 集合從用戶態(tài)復制到內(nèi)核態(tài),并在內(nèi)核中遍歷所有 fd 來檢查其狀態(tài)。隨著 fd 數(shù)量的增加,輪詢的開銷會線性增長,導致性能顯著下降。
  • epoll 的事件驅(qū)動機制:- epoll 使用基于事件回調(diào)的機制。內(nèi)核會維護一個就緒隊列,只關(guān)注那些狀態(tài)發(fā)生變化的 fd(即活躍的 fd)。一旦檢測到epoll管理的socket描述符就緒時,內(nèi)核會采用類似 callback 的回調(diào)機制,將其加入就緒隊列,epoll_wait 只需從隊列中獲取就緒的 fd,而不需要遍歷所有 fd。這種機制使得 epoll 的性能不會隨著 fd 數(shù)量的增加而顯著下降。

(2) 避免頻繁的用戶態(tài)與內(nèi)核態(tài)數(shù)據(jù)拷貝

  • select 和 poll 的數(shù)據(jù)拷貝問題: 每次調(diào)用 select 或 poll 時,都需要將整個 fd 集合從用戶態(tài)復制到內(nèi)核態(tài),調(diào)用結(jié)束后再將結(jié)果從內(nèi)核態(tài)復制回用戶態(tài)。這種頻繁的數(shù)據(jù)拷貝在高并發(fā)場景下會帶來較大的性能開銷。
  • epoll 的優(yōu)化: epoll 使用了內(nèi)存映射( mmap )技術(shù),這樣便徹底省掉了這些socket描述符在系統(tǒng)調(diào)用時拷貝的開銷(因為從用戶空間到內(nèi)核空間需要拷貝操作)。mmap將用戶空間的一塊地址和內(nèi)核空間的一塊地址同時映射到相同的一塊物理內(nèi)存地址(不管是用戶空間還是內(nèi)核空間都是虛擬地址,最終要通過地址映射映射到物理地址),使得這塊物理內(nèi)存對內(nèi)核和對用戶均可見,減少用戶態(tài)和內(nèi)核態(tài)之間的數(shù)據(jù)交換,不需要依賴拷貝,這樣子內(nèi)核可以直接看到epoll監(jiān)聽的socket描述符,效率極高。

(3) 支持更大的并發(fā)連接數(shù)

  • select 的 fd 數(shù)量限制: select 使用 fd_set 結(jié)構(gòu),其大小通常被限制為 1024(由 __FD_SETSIZE 定義),這意味著它最多只能同時監(jiān)視 1024 個 fd。雖然可以通過修改內(nèi)核頭文件并重新編譯內(nèi)核來擴大這一限制,但這并不能從根本上解決問題。
  • poll 的改進與局限: poll 使用 pollfd 結(jié)構(gòu),理論上可以支持更多的 fd,但它仍然需要遍歷所有 fd,性能會隨著 fd 數(shù)量的增加而下降。
  • epoll 的無限制支持: epoll 沒有 fd 數(shù)量的硬性限制,適合高并發(fā)場景,能夠輕松支持數(shù)萬甚至數(shù)十萬的并發(fā)連接。

四、總結(jié)

select,poll,epoll都是IO多路復用機制,即可以監(jiān)視多個描述符,一旦某個描述符就緒(讀或?qū)懢途w),能夠通知程序進行相應讀寫操作。 但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現(xiàn)會負責把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

  • select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機制帶來的性能提升。
  • select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。
責任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2020-10-13 07:51:03

五種IO模型

2023-01-09 10:04:47

IO多路復用模型

2023-12-13 09:45:49

模型程序

2020-10-14 09:11:44

IO 多路復用實現(xiàn)機

2021-05-31 06:50:47

SelectPoll系統(tǒng)

2017-01-17 14:21:27

LinuxIO模型Unix

2023-05-05 09:48:14

LinuxIO模型

2024-08-08 14:57:32

2019-07-25 07:14:03

LinuxSync操作系統(tǒng)

2021-11-23 10:30:35

Android技術(shù)代碼

2023-05-09 11:13:09

IO模型語言

2023-11-07 08:19:35

IO多路復用磁盤、

2022-08-26 00:21:44

IO模型線程

2009-12-01 09:18:22

Linux版本

2025-06-06 00:33:00

2012-08-22 14:05:25

Linux服務(wù)器

2012-08-23 10:01:03

Linux服務(wù)器

2023-03-01 14:32:31

redisIOEpoll

2024-09-26 16:01:52

2010-06-13 15:22:21

Linux網(wǎng)絡(luò)測試
點贊
收藏

51CTO技術(shù)棧公眾號

我看黄色一级片| 欧美日韩一区二区三区在线视频 | 色婷婷久久av| 日韩欧美色视频| 欧美私密网站| 中文字幕一区二区三| 国产传媒一区二区| 中文字幕第三页| 99re国产精品| 久久精品99无色码中文字幕 | 国产精品久久久久久无人区| 激情久久综合| 在线性视频日韩欧美| 无码国产69精品久久久久网站| 国产另类xxxxhd高清| 一区二区欧美精品| 亚洲精品久久区二区三区蜜桃臀| 亚洲AV无码精品国产| 日日欢夜夜爽一区| 欧美精品videofree1080p| 日本免费www| 久久综合五月婷婷| 日韩三级.com| www.国产福利| 成人午夜在线| 一本一道久久a久久精品| 亚洲中文字幕无码一区二区三区 | 久久99亚洲精品| 长河落日免费高清观看| 日韩有码av| 亚洲电影免费观看| 曰本三级日本三级日本三级| 日韩av懂色| 日本韩国欧美国产| 欧美三级一级片| eeuss鲁一区二区三区| 自拍偷拍亚洲欧美日韩| 神马欧美一区二区| 久久电影中文字幕| 26uuu欧美日本| 国产在线一区二| 亚洲欧美另类综合| 国产成人综合在线播放| 91网站在线看| 国产精品久久久久毛片| 精品一区二区久久久| 国产精品第100页| 中文字幕日韩免费| 老司机亚洲精品| 日本成人在线视频网址| 国产成人亚洲精品自产在线| 亚洲成人资源| 69av成年福利视频| 亚洲黄色小说图片| 国产精品日韩| 日韩美女在线观看| 国产日韩在线免费观看| 日韩 欧美一区二区三区| 日本久久亚洲电影| 欧美一级做a爰片免费视频| 丝袜美腿亚洲一区| 国产日韩精品视频| 国产精品伦一区二区三区| 韩国一区二区三区| 99久久一区三区四区免费| av男人天堂av| 成人动漫在线一区| 久久久久久艹| wwwxxx在线观看| 亚洲欧美一区二区不卡| wwwwww欧美| 国产伦理精品| 色老综合老女人久久久| 一道本在线免费视频| 国内不卡的一区二区三区中文字幕| 337p亚洲精品色噜噜| 日本女人性视频| 久久久久观看| 伊人精品在线观看| 欧美黑人性猛交xxx| 亚洲国产精品第一区二区| 1769国产精品| 亚洲熟妇无码久久精品| 国产凹凸在线观看一区二区| 久精品国产欧美| 在线免费观看黄色av| 亚洲精品成a人| 91视频最新入口| 激情亚洲小说| 欧美精品一区二区三区一线天视频| 国产制服丝袜在线| 偷偷www综合久久久久久久| 欧美日韩国产二区| 午夜一区二区三区四区| 国产一区二区三区高清播放| 久久久综合香蕉尹人综合网| 男人影院在线观看| 精品国产1区2区| 成人综合久久网| 久久91在线| 日韩中文在线视频| 成年人视频在线免费看| 久久精品国产精品亚洲综合| 韩国一区二区三区美女美女秀| 调教视频免费在线观看| 亚洲成人动漫精品| 五月天av在线播放| 日韩影视高清在线观看| 欧美精品一区二区免费| 久久久久99精品成人片我成大片| 狠狠色狠狠色综合| 欧美视频小说| 国产又色又爽又黄刺激在线视频| 欧美自拍丝袜亚洲| 最近日本中文字幕| 亚洲欧美在线专区| 国产精品高精视频免费| 天天干天天操av| 亚洲精品乱码久久久久久黑人| 乱子伦视频在线看| 久久久精品国产**网站| 九九精品视频在线| 在线免费观看日韩视频| 久久久久久久久99精品| 久无码久无码av无码| 国产精品美女久久久久人| 在线观看国产成人av片| 毛片基地在线观看| 国产不卡高清在线观看视频| 国产日产欧美一区二区| 日韩福利在线观看| 最近2019中文字幕在线高清| www.欧美色| 91色在线porny| 日本wwwcom| 96sao在线精品免费视频| 欧美成人精品xxx| 国产精品视频a| 中文子幕无线码一区tr| 91在线视频观看免费| 婷婷成人在线| 秋霞av国产精品一区| 天天摸天天干天天操| 欧美日韩精品在线| 午夜免费福利影院| 亚洲精品韩国| 久久艹中文字幕| 三级在线看中文字幕完整版| 亚洲国内精品在线| 日韩欧美成人一区二区三区| 久久网站最新地址| 国产精品亚洲αv天堂无码| 亚洲第一论坛sis| 欧美在线一区二区视频| 欧美日本韩国一区二区| 一本到一区二区三区| 97伦伦午夜电影理伦片| 水野朝阳av一区二区三区| 日韩和欧美的一区二区| 在线高清欧美| 欧美日韩成人在线视频| 日日夜夜精品免费| 欧美性xxxx极品hd欧美风情| brazzers精品成人一区| 肉丝袜脚交视频一区二区| 亚洲成人18| 99精品国产九九国产精品| 欧美精品日韩三级| 农村少妇久久久久久久| 日韩欧美黄色动漫| 国产三级黄色片| 国产黄色精品网站| 日韩a∨精品日韩在线观看| 免费观看不卡av| 国产精品热视频| 麻豆av在线免费观看| 亚洲精品第一国产综合精品| 久久久久久av无码免费看大片| 国产精品福利影院| 黄色av电影网站| 爽爽淫人综合网网站| 特级黄色录像片| 欧美电影完整版在线观看| 国产精品美女久久久久av超清| 日本私人网站在线观看| 欧美精品乱人伦久久久久久| 国产第100页| 中文在线一区二区| 亚洲成年人在线观看| 青青草视频一区| 91黄色在线看| 99精品电影| 国严精品久久久久久亚洲影视| 色猫猫成人app| 久久久久久久久网站| www.亚洲.com| 亚洲精品美女在线| 国产精品女人久久久| 天天综合色天天| 欧美色视频一区二区三区在线观看| 国产91对白在线观看九色| 日本人视频jizz页码69| 日韩视频精品在线观看| 亚洲一区三区电影在线观看| 丝袜久久网站| 91精品天堂| 成人午夜在线| 青青草一区二区| heyzo在线播放| 久久av红桃一区二区小说| 人人九九精品| 欧美精品一区二区在线观看| 91极品身材尤物theporn| 色综合夜色一区| 久久久无码精品亚洲国产| 国产精品毛片久久久久久久| 成人网站免费观看| 成人免费av在线| 亚洲精品在线网址| 久久精品国产999大香线蕉| 欧美 日韩精品| 亚洲青涩在线| 久久国产午夜精品理论片最新版本| 欧美激情电影| 日本不卡在线播放| 日韩精品丝袜美腿| 国内精品视频在线播放| 亚洲精选av| 亚洲一区二区三区成人在线视频精品 | 18欧美亚洲精品| 国产又粗又黄又猛| 久久久久成人黄色影片| 91黄色免费视频| 不卡的av电影在线观看| 久久精品无码专区| 福利一区福利二区| 香蕉久久久久久av成人| 国产成人啪免费观看软件 | 成人av免费在线观看| 亚洲成人激情小说| 国产精品一区免费视频| 思思久久精品视频| 麻豆精品国产91久久久久久| 国产一二三四在线视频| 日韩国产一区二| 在线观看的毛片| 另类的小说在线视频另类成人小视频在线 | 非洲一级黄色片| 欧美激情一区二区| 黄色国产在线播放| 国产精品不卡在线| 99久久99久久精品国产| 亚洲精品一卡二卡| 精品无码免费视频| 五月天欧美精品| 91在线看视频| 一本大道久久a久久精二百| 免费视频久久久| 在线观看成人小视频| 夜夜爽8888| 日韩欧美国产电影| 五月婷婷六月丁香| 亚洲女人天堂av| av大全在线免费看| 久久av.com| 麻豆成全视频免费观看在线看| 青草青草久热精品视频在线网站| 欧洲成人一区| 91成人在线看| 色爱综合av| 亚洲一区3d动漫同人无遮挡 | 99久久精品国产色欲| 欧美mv和日韩mv的网站| 四虎精品成人影院观看地址| 一区三区二区视频| 91精选在线| 国产91精品视频在线观看| av成人免费看| 99热在线播放| 亚洲精品一级二级三级| 亚洲一区三区电影在线观看| 国内自拍视频一区二区三区| 日本一本二本在线观看| 久久99精品国产麻豆婷婷 | 久久精品亚洲精品国产欧美kt∨| 国产又粗又猛又爽又黄的视频四季 | 欧美特大特白屁股xxxx| 国产色综合天天综合网| 大香伊人久久精品一区二区| 蜜桃91精品入口| 久久久久国产| 可以免费观看av毛片| 国产一区不卡视频| 永久免费看mv网站入口78| 亚洲另类中文字| 一级片在线免费播放| 亚洲大胆人体av| 日本中文字幕视频在线| 97香蕉超级碰碰久久免费软件 | 粉嫩高清一区二区三区精品视频| 深爱激情综合网| 欧美日韩视频免费| 美女一区二区三区在线观看| 中文字幕在线视频播放| 中文字幕亚洲不卡| 国产精品男女视频| 精品三级在线观看| 永久av在线| 国产成人精品视频| 九九热hot精品视频在线播放| 在线国产伦理一区| 久久国产主播| 在线精品一区二区三区| 一区二区三区视频在线看| 亚洲天堂999| 亚洲精品日韩欧美| gogo久久| 国产a一区二区| 婷婷丁香综合| 自拍偷拍一区二区三区四区| 久久精品亚洲精品国产欧美| 国产成人亚洲欧洲在线| 精品国产乱码久久久久久图片| 老司机午夜在线| 国产精品亚洲网站| 欧美艳星介绍134位艳星| 日本三级免费观看| 久久在线观看免费| 日韩精品一区二区亚洲av| 亚洲国模精品私拍| 成人免费观看在线观看| 国产伦精品一区二区三区照片91 | 欧美一级精品| 国产视频在线视频| 久久久久久久久免费| 国产精品100| 亚洲欧美国产日韩中文字幕| 黄色在线免费观看网站| 国产无套精品一区二区| 伊人久久大香线蕉综合热线 | 日韩 欧美 高清| xfplay精品久久| 99超碰在线观看| 国产亚洲欧洲高清| jizzyou欧美16| 亚洲在线欧美| 久久99精品久久久| wwwav国产| 欧美videos大乳护士334| 欧美韩日亚洲| 久久99国产精品| 麻豆91精品| 香蕉视频久久久| 欧美日韩国产成人在线免费| 女女色综合影院| 5g国产欧美日韩视频| 好吊视频一区二区三区四区| 欧美激情一区二区三区p站| 亚洲高清一区二区三区| 天天综合网在线| 日本精品视频网站| 日韩理论电影大全| 在线免费观看av网| 亚洲国产色一区| 欧洲一级在线观看| 国产精品久久久久久亚洲调教| 三级电影一区| 99热这里只有精品2| 亚洲va韩国va欧美va精品| 视频二区在线| 国产精品无码专区在线观看| 91久久久精品国产| 久久性爱视频网站| 91传媒视频在线播放| 黄色av网站在线播放| 国产精品免费一区二区三区| 香蕉亚洲视频| 国产小视频你懂的| 亚洲高清一二三区| 成人深夜福利| 成人在线播放网址| 国产性做久久久久久| 国产黄色免费大片| 欧美制服第一页| 亚洲a在线视频| a天堂视频在线观看| 欧美日韩精品欧美日韩精品一| 大地资源网3页在线观看| 精品一区二区不卡| 久久国产日韩欧美精品| 亚洲国产精一区二区三区性色| 亚洲香蕉成人av网站在线观看| 高清一区二区中文字幕| 国产91美女视频| 亚洲天堂2014| 国产毛片av在线| 国产精品一区在线观看| 麻豆视频一区二区| 国产剧情在线视频| 欧美精品18videosex性欧美|