自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

徹底理解 IO 多路復(fù)用實(shí)現(xiàn)機(jī)制

開發(fā) 前端
本篇重點(diǎn)學(xué)習(xí)理解IO多路復(fù)用的底層實(shí)現(xiàn)機(jī)制。

[[346138]]

 前言

  • BIO 、NIO 、AIO 總結(jié)
  • Unix網(wǎng)絡(luò)編程中的五種IO模型

為了加深對(duì) I/O多路復(fù)用機(jī)制 的理解,以及了解到多路復(fù)用也有局限性,本著打破砂鍋問到底的精神,前面我們講了BIO、NIO、AIO的基本概念以及一些常見問題,同時(shí)也回顧了Unix網(wǎng)絡(luò)編程中的五種IO模型。本篇重點(diǎn)學(xué)習(xí)理解IO多路復(fù)用的底層實(shí)現(xiàn)機(jī)制。

概念說明
IO 多路復(fù)用有三種實(shí)現(xiàn),在介紹select、poll、epoll之前,首先介紹一下Linux操作系統(tǒng)中基礎(chǔ)的概念:

  • 用戶空間和內(nèi)核空間
  • 進(jìn)程切換
  • 進(jìn)程的阻塞
  • 文件描述符
  • 緩存 I/O

用戶空間 / 內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器,那么對(duì)32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方)。 操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。

針對(duì)linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用,稱為用戶空間。

進(jìn)程切換
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換。因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的,并且進(jìn)程切換是非常耗費(fèi)資源的。

從一個(gè)進(jìn)程的運(yùn)行轉(zhuǎn)到另一個(gè)進(jìn)程上運(yùn)行,這個(gè)過程中經(jīng)過下面這些變化:

  1. 保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其他寄存器。
  2. 更新PCB信息。
  3. 把進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。
  4. 選擇另一個(gè)進(jìn)程執(zhí)行,并更新其PCB。
  5. 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
  6. 恢復(fù)處理機(jī)上下文。

進(jìn)程阻塞
正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?,進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得了CPU資源),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。

文件描述符
文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。

緩存I/O
緩存I/O又稱為標(biāo)準(zhǔn)I/O,大多數(shù)文件系統(tǒng)的默認(rèn)I/O操作都是緩存I/O。在Linux的緩存I/O機(jī)制中,操作系統(tǒng)會(huì)將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,即數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。

緩存 I/O 的缺點(diǎn):

數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。

什么是IO多路復(fù)用?

  • IO 多路復(fù)用是一種同步IO模型,實(shí)現(xiàn)一個(gè)線程可以監(jiān)視多個(gè)文件句柄;
  • 一旦某個(gè)文件句柄就緒,就能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作;
  • 沒有文件句柄就緒就會(huì)阻塞應(yīng)用程序,交出CPU。

多路是指網(wǎng)絡(luò)連接,復(fù)用指的是同一個(gè)線程

為什么有IO多路復(fù)用機(jī)制?
沒有IO多路復(fù)用機(jī)制時(shí),有BIO、NIO兩種實(shí)現(xiàn)方式,但它們都有一些問題

同步阻塞(BIO)
服務(wù)端采用單線程,當(dāng) accept 一個(gè)請(qǐng)求后,在 recv 或 send 調(diào)用阻塞時(shí),將無法 accept 其他請(qǐng)求(必須等上一個(gè)請(qǐng)求處理 recv 或 send 完 )(無法處理并發(fā))

  1. // 偽代碼描述 
  2. while (true) { 
  3.  // accept阻塞 
  4.     client_fd = accept(listen_fd); 
  5.     fds.append(client_fd); 
  6.     for (fd in fds) { 
  7.      // recv阻塞(會(huì)影響上面的accept) 
  8.         if (recv(fd)) { 
  9.          // logic 
  10.         } 
  11.     } 
  • 服務(wù)端采用多線程,當(dāng) accept 一個(gè)請(qǐng)求后,開啟線程進(jìn)行 recv,可以完成并發(fā)處理,但隨著請(qǐng)求數(shù)增加需要增加系統(tǒng)線程,大量的線程占用很大的內(nèi)存空間,并且線程切換會(huì)帶來很大的開銷,10000個(gè)線程真正發(fā)生讀寫實(shí)際的線程數(shù)不會(huì)超過20%,每次accept都開一個(gè)線程也是一種資源浪費(fèi)。
  1. // 偽代碼描述 
  2. while(true) { 
  3.   // accept阻塞 
  4.   client_fd = accept(listen_fd) 
  5.   // 開啟線程read數(shù)據(jù)(fd增多導(dǎo)致線程數(shù)增多) 
  6.   new Thread func() { 
  7.     // recv阻塞(多線程不影響上面的accept) 
  8.     if (recv(fd)) { 
  9.       // logic 
  10.     } 
  11.   }   

同步非阻塞(NIO)

  • 服務(wù)器端當(dāng) accept 一個(gè)請(qǐng)求后,加入 fds 集合,每次輪詢一遍 fds 集合 recv (非阻塞)數(shù)據(jù),沒有數(shù)據(jù)則立即返回錯(cuò)誤,每次輪詢所有 fd (包括沒有發(fā)生讀寫實(shí)際的 fd)會(huì)很浪費(fèi) CPU。
  1. // 偽代碼描述 
  2. while(true) { 
  3.   // accept非阻塞(cpu一直忙輪詢) 
  4.   client_fd = accept(listen_fd) 
  5.   if (client_fd != null) { 
  6.     // 有人連接 
  7.     fds.append(client_fd) 
  8.   } else { 
  9.     // 無人連接 
  10.   }   
  11.   for (fd in fds) { 
  12.     // recv非阻塞 
  13.     setNonblocking(client_fd) 
  14.     // recv 為非阻塞命令 
  15.     if (len = recv(fd) && len > 0) { 
  16.       // 有讀寫數(shù)據(jù) 
  17.       // logic 
  18.     } else { 
  19.        無讀寫數(shù)據(jù) 
  20.     } 
  21.   }   

IO多路復(fù)用
服務(wù)器端采用單線程通過 select/poll/epoll 等系統(tǒng)調(diào)用獲取 fd 列表,遍歷有事件的 fd 進(jìn)行 accept/recv/send ,使其能支持更多的并發(fā)連接請(qǐng)求。

  1. // 偽代碼描述 
  2. while(true) { 
  3.   // 通過內(nèi)核獲取有讀寫事件發(fā)生的fd,只要有一個(gè)則返回,無則阻塞 
  4.   // 整個(gè)過程只在調(diào)用select、poll、epoll這些調(diào)用的時(shí)候才會(huì)阻塞,accept/recv是不會(huì)阻塞 
  5.   for (fd in select(fds)) { 
  6.     if (fd == listen_fd) { 
  7.         client_fd = accept(listen_fd) 
  8.         fds.append(client_fd) 
  9.     } elseif (len = recv(fd) && len != -1) {  
  10.       // logic 
  11.     } 
  12.   }   

IO多路復(fù)用的三種實(shí)現(xiàn)

  • select
  • poll
  • epoll

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

select調(diào)用過程

(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來說,其等待隊(duì)列是sk->sk_sleep(注意把進(jìn)程掛到等待隊(duì)列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù)據(jù)(磁盤設(shè)備)后,會(huì)喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程,這時(shí)current便被喚醒了。

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

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

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

select函數(shù)接口

  1. #include <sys/select.h> 
  2. #include <sys/time.h> 
  3.  
  4. #define FD_SETSIZE 1024 
  5. #define NFDBITS (8 * sizeof(unsigned long)) 
  6. #define __FDSET_LONGS (FD_SETSIZE/NFDBITS) 
  7.  
  8. // 數(shù)據(jù)結(jié)構(gòu) (bitmap) 
  9. typedef struct { 
  10.     unsigned long fds_bits[__FDSET_LONGS]; 
  11. } fd_set; 
  12.  
  13. // API 
  14. int select
  15.     int max_fd,  
  16.     fd_set *readset,  
  17.     fd_set *writeset,  
  18.     fd_set *exceptset,  
  19.     struct timeval *timeout 
  20. )                              // 返回值就緒描述符的數(shù)目 
  21.  
  22. FD_ZERO(int fd, fd_set* fds)   // 清空集合 
  23. FD_SET(int fd, fd_set* fds)    // 將給定的描述符加入集合 
  24. FD_ISSET(int fd, fd_set* fds)  // 判斷指定描述符是否在集合中  
  25. FD_CLR(int fd, fd_set* fds)    // 將給定的描述符從文件中刪除   

select使用示例

  1. int main() { 
  2.   /* 
  3.    * 這里進(jìn)行一些初始化的設(shè)置, 
  4.    * 包括socket建立,地址的設(shè)置等, 
  5.    */ 
  6.  
  7.   fd_set read_fs, write_fs; 
  8.   struct timeval timeout; 
  9.   int max = 0;  // 用于記錄最大的fd,在輪詢中時(shí)刻更新即可 
  10.  
  11.   // 初始化比特位 
  12.   FD_ZERO(&read_fs); 
  13.   FD_ZERO(&write_fs); 
  14.  
  15.   int nfds = 0; // 記錄就緒的事件,可以減少遍歷的次數(shù) 
  16.   while (1) { 
  17.     // 阻塞獲取 
  18.     // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài) 
  19.     nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout); 
  20.     // 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生 
  21.     for (int i = 0; i <= max && nfds; ++i) { 
  22.       if (i == listenfd) { 
  23.          --nfds; 
  24.          // 這里處理accept事件 
  25.          FD_SET(i, &read_fd);//將客戶端socket加入到集合中 
  26.       } 
  27.       if (FD_ISSET(i, &read_fd)) { 
  28.         --nfds; 
  29.         // 這里處理read事件 
  30.       } 
  31.       if (FD_ISSET(i, &write_fd)) { 
  32.          --nfds; 
  33.         // 這里處理write事件 
  34.       } 
  35.     } 
  36.   } 

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

  • 單個(gè)進(jìn)程所打開的FD是有限制的,通過 FD_SETSIZE 設(shè)置,默認(rèn)1024 ;
  • 每次調(diào)用 select,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在 fd 很多時(shí)會(huì)很大;

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

  • 對(duì) socket 掃描時(shí)是線性掃描,采用輪詢的方法,效率較低(高并發(fā))

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

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

poll函數(shù)接口

  1. #include <poll.h> 
  2. // 數(shù)據(jù)結(jié)構(gòu) 
  3. struct pollfd { 
  4.     int fd;                         // 需要監(jiān)視的文件描述符 
  5.     short events;                   // 需要內(nèi)核監(jiān)視的事件 
  6.     short revents;                  // 實(shí)際發(fā)生的事件 
  7. }; 
  8.  
  9. // API 
  10. int poll(struct pollfd fds[], nfds_t nfds, int timeout); 

poll使用示例

  1. // 先宏定義長(zhǎng)度 
  2. #define MAX_POLLFD_LEN 4096   
  3.  
  4. int main() { 
  5.   /* 
  6.    * 在這里進(jìn)行一些初始化的操作, 
  7.    * 比如初始化數(shù)據(jù)和socket等。 
  8.    */ 
  9.  
  10.   int nfds = 0; 
  11.   pollfd fds[MAX_POLLFD_LEN]; 
  12.   memset(fds, 0, sizeof(fds)); 
  13.   fds[0].fd = listenfd; 
  14.   fds[0].events = POLLRDNORM; 
  15.   int max  = 0;  // 隊(duì)列的實(shí)際長(zhǎng)度,是一個(gè)隨時(shí)更新的,也可以自定義其他的 
  16.   int timeout = 0; 
  17.  
  18.   int current_size = max
  19.   while (1) { 
  20.     // 阻塞獲取 
  21.     // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài) 
  22.     nfds = poll(fds, max+1, timeout); 
  23.     if (fds[0].revents & POLLRDNORM) { 
  24.         // 這里處理accept事件 
  25.         connfd = accept(listenfd); 
  26.         //將新的描述符添加到讀描述符集合中 
  27.     } 
  28.     // 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生 
  29.     for (int i = 1; i < max; ++i) {      
  30.       if (fds[i].revents & POLLRDNORM) {  
  31.          sockfd = fds[i].fd 
  32.          if ((n = read(sockfd, buf, MAXLINE)) <= 0) { 
  33.             // 這里處理read事件 
  34.             if (n == 0) { 
  35.                 close(sockfd); 
  36.                 fds[i].fd = -1; 
  37.             } 
  38.          } else { 
  39.              // 這里處理write事件      
  40.          } 
  41.          if (--nfds <= 0) { 
  42.             break;        
  43.          }    
  44.       } 
  45.     } 
  46.   } 

poll缺點(diǎn)
它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲(chǔ)的,但是同樣有缺點(diǎn):

  • 每次調(diào)用 poll ,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在 fd 很多時(shí)會(huì)很大;
  • 對(duì) socket 掃描是線性掃描,采用輪詢的方法,效率較低(高并發(fā)時(shí))

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

epoll函數(shù)接口
當(dāng)某一進(jìn)程調(diào)用epoll_create方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。eventpoll結(jié)構(gòu)體如下所示:

  1. #include <sys/epoll.h> 
  2.  
  3. // 數(shù)據(jù)結(jié)構(gòu) 
  4. // 每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體 
  5. // 用于存放通過epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件 
  6. // epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可 
  7. struct eventpoll { 
  8.     /*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/ 
  9.     struct rb_root  rbr; 
  10.     /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/ 
  11.     struct list_head rdlist; 
  12. }; 
  13.  
  14. // API 
  15. int epoll_create(int size); // 內(nèi)核中間加一個(gè) ep 對(duì)象,把所有需要監(jiān)聽的 socket 都放到 ep 對(duì)象中 
  16. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 負(fù)責(zé)把 socket 增加、刪除到內(nèi)核紅黑樹 
  17. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 負(fù)責(zé)檢測(cè)可讀隊(duì)列,沒有可讀 socket 則阻塞進(jìn)程 

每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件。這些事件都會(huì)掛載在紅黑樹中,如此,重復(fù)添加的事件就可以通過紅黑樹而高效的識(shí)別出來(紅黑樹的插入時(shí)間效率是lgn,其中n為紅黑樹元素個(gè)數(shù))。

而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。

在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:

  1. struct epitem{ 
  2.     struct rb_node  rbn;//紅黑樹節(jié)點(diǎn) 
  3.     struct list_head    rdllink;//雙向鏈表節(jié)點(diǎn) 
  4.     struct epoll_filefd  ffd;  //事件句柄信息 
  5.     struct eventpoll *ep;    //指向其所屬的eventpoll對(duì)象 
  6.     struct epoll_event event; //期待發(fā)生的事件類型 

當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。

從上面的講解可知:通過紅黑樹和雙鏈表數(shù)據(jù)結(jié)構(gòu),并結(jié)合回調(diào)機(jī)制,造就了epoll的高效。 講解完了Epoll的機(jī)理,我們便能很容易掌握epoll的用法了。一句話描述就是:三步曲。

  • 第一步:epoll_create()系統(tǒng)調(diào)用。此調(diào)用返回一個(gè)句柄,之后所有的使用都依靠這個(gè)句柄來標(biāo)識(shí)。
  • 第二步:epoll_ctl()系統(tǒng)調(diào)用。通過此調(diào)用向epoll對(duì)象中添加、刪除、修改感興趣的事件,返回0標(biāo)識(shí)成功,返回-1表示失敗。
  • 第三部:epoll_wait()系統(tǒng)調(diào)用。通過此調(diào)用收集收集在epoll監(jiān)控中已經(jīng)發(fā)生的事件。

epoll使用示例

  1. int main(int argc, char* argv[]) 
  2.    /* 
  3.    * 在這里進(jìn)行一些初始化的操作, 
  4.    * 比如初始化數(shù)據(jù)和socket等。 
  5.    */ 
  6.  
  7.     // 內(nèi)核中創(chuàng)建ep對(duì)象 
  8.     epfd=epoll_create(256); 
  9.     // 需要監(jiān)聽的socket放到ep中 
  10.     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 
  11.   
  12.     while(1) { 
  13.       // 阻塞獲取 
  14.       nfds = epoll_wait(epfd,events,20,0); 
  15.       for(i=0;i<nfds;++i) { 
  16.           if(events[i].data.fd==listenfd) { 
  17.               // 這里處理accept事件 
  18.               connfd = accept(listenfd); 
  19.               // 接收新連接寫到內(nèi)核對(duì)象中 
  20.               epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); 
  21.           } else if (events[i].events&EPOLLIN) { 
  22.               // 這里處理read事件 
  23.               read(sockfd, BUF, MAXLINE); 
  24.               //讀完后準(zhǔn)備寫 
  25.               epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 
  26.           } else if(events[i].events&EPOLLOUT) { 
  27.               // 這里處理write事件 
  28.               write(sockfd, BUF, n); 
  29.               //寫完后準(zhǔn)備讀 
  30.               epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 
  31.           } 
  32.       } 
  33.     } 
  34.     return 0; 

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

  • 沒有最大并發(fā)連接的限制,能打開的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個(gè)端口);
  • 效率提升,不是輪詢的方式,不會(huì)隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會(huì)調(diào)用callback函數(shù);即Epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll;
  • 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷。

epoll缺點(diǎn)

  • epoll只能工作在 linux 下

epoll LT 與 ET 模式的區(qū)別
epoll 有 EPOLLLT 和 EPOLLET 兩種觸發(fā)模式,LT 是默認(rèn)的模式,ET 是 “高速” 模式。

  • LT 模式下,只要這個(gè) fd 還有數(shù)據(jù)可讀,每次 epoll_wait 都會(huì)返回它的事件,提醒用戶程序去操作;
  • ET 模式下,它只會(huì)提示一次,直到下次再有數(shù)據(jù)流入之前都不會(huì)再提示了,無論 fd 中是否還有數(shù)據(jù)可讀。所以在 ET 模式下,read 一個(gè) fd 的時(shí)候一定要把它的 buffer 讀完,或者遇到 EAGIN 錯(cuò)誤。

epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊(cè)fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來激活該fd,epoll_wait便可以收到通知。

select/poll/epoll之間的區(qū)別
select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步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)

epoll是Linux目前大規(guī)模網(wǎng)絡(luò)并發(fā)程序開發(fā)的首選模型。在絕大多數(shù)情況下性能遠(yuǎn)超select和poll。目前流行的高性能web服務(wù)器Nginx正式依賴于epoll提供的高效網(wǎng)絡(luò)套接字輪詢服務(wù)。但是,在并發(fā)連接不高的情況下,多線程+阻塞I/O方式可能性能更好。

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

  • select:?jiǎn)蝹€(gè)進(jìn)程所能打開的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個(gè)整數(shù)的大?。ㄔ?2位的機(jī)器上,大小就是32_32,同理64位機(jī)器上FD_SETSIZE為32_64),當(dāng)然我們可以對(duì)進(jìn)行修改,然后重新編譯內(nèi)核,但是性能可能會(huì)受到影響,這需要進(jìn)一步的測(cè)試。
  • poll:poll本質(zhì)上和select沒有區(qū)別,但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲(chǔ)的。
  • epoll:雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機(jī)器上可以打開10萬左右的連接,2G內(nèi)存的機(jī)器可以打開20萬左右的連接。

FD劇增后帶來的IO效率問題

  • select:因?yàn)槊看握{(diào)用時(shí)都會(huì)對(duì)連接進(jìn)行線性遍歷,所以隨著FD的增加會(huì)造成遍歷速度慢的“線性下降性能問題”。
  • poll:同上
  • epoll:因?yàn)閑poll內(nèi)核中實(shí)現(xiàn)是根據(jù)每個(gè)fd上的callback函數(shù)來實(shí)現(xiàn)的,只有活躍的socket才會(huì)主動(dòng)調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會(huì)有性能問題。

消息傳遞方式

  • select:內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動(dòng)作
  • poll:同上
  • epoll:epoll通過內(nèi)核和用戶空間共享一塊內(nèi)存來實(shí)現(xiàn)的。

總結(jié)
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ī)制帶來的性能提升。

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

高頻面試題
什么是IO多路復(fù)用?
看完上面的文章,相信你可以回答出來了。

nginx/redis 所使用的IO模型是什么?
Nginx的IO模型
Nginx 支持多種并發(fā)模型,并發(fā)模型的具體實(shí)現(xiàn)根據(jù)系統(tǒng)平臺(tái)而有所不同。

在支持多種并發(fā)模型的平臺(tái)上,nginx 自動(dòng)選擇最高效的模型。但我們也可以使用 use 指令在配置文件中顯式地定義某個(gè)并發(fā)模型。

NGINX中支持的并發(fā)模型:

1、select
IO多路復(fù)用、標(biāo)準(zhǔn)并發(fā)模型。在編譯 nginx 時(shí),如果所使用的系統(tǒng)平臺(tái)沒有更高效的并發(fā)模型,select 模塊將被自動(dòng)編譯。configure 腳本的選項(xiàng):–with-select_module 和 --without-select_module 可被用來強(qiáng)制性地開啟或禁止 select 模塊的編譯

2、poll
IO多路復(fù)用、標(biāo)準(zhǔn)并發(fā)模型。與 select 類似,在編譯 nginx 時(shí),如果所使用的系統(tǒng)平臺(tái)沒有更高效的并發(fā)模型,poll 模塊將被自動(dòng)編譯。configure 腳本的選項(xiàng):–with-poll_module 和 --without-poll_module 可用于強(qiáng)制性地開啟或禁止 poll 模塊的編譯

3、epoll
IO多路復(fù)用、高效并發(fā)模型,可在 Linux 2.6+ 及以上內(nèi)核可以使用

4、kqueue
IO多路復(fù)用、高效并發(fā)模型,可在 FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0, and Mac OS X 平臺(tái)中使用

5、/dev/poll
高效并發(fā)模型,可在 Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+, and Tru64 UNIX 5.1A+ 平臺(tái)使用

6、eventport
高效并發(fā)模型,可用于 Solaris 10 平臺(tái),PS:由于一些已知的問題,建議 使用/dev/poll替代。

Redis IO多路復(fù)用技術(shù)
redis 是一個(gè)單線程卻性能非常好的內(nèi)存數(shù)據(jù)庫, 主要用來作為緩存系統(tǒng)。 redis 采用網(wǎng)絡(luò)IO多路復(fù)用技術(shù)來保證在多連接的時(shí)候, 系統(tǒng)的高吞吐量。

為什么 Redis 中要使用 I/O 多路復(fù)用這種技術(shù)呢?

首先,Redis 是跑在單線程中的,所有的操作都是按照順序線性執(zhí)行的,但是由于讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會(huì)導(dǎo)致某一文件的 I/O 阻塞導(dǎo)致整個(gè)進(jìn)程無法對(duì)其它客戶提供服務(wù),而 I/O 多路復(fù)用 就是為了解決這個(gè)問題而出現(xiàn)的。

redis的io模型主要是基于epoll實(shí)現(xiàn)的,不過它也提供了 select和kqueue的實(shí)現(xiàn),默認(rèn)采用epoll。

select、poll、epoll之間的區(qū)別
看完上面的文章,相信你可以回答出來了。

epoll 水平觸發(fā)(LT)與 邊緣觸發(fā)(ET)的區(qū)別?
EPOLL事件有兩種模型:

  • Edge Triggered (ET) 邊緣觸發(fā)只有數(shù)據(jù)到來,才觸發(fā),不管緩存區(qū)中是否還有數(shù)據(jù)。
  • Level Triggered (LT) 水平觸發(fā)只要有數(shù)據(jù)都會(huì)觸發(fā)。

看完上面的文章,相信你可以回答出來了。

 

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2023-11-07 08:19:35

IO多路復(fù)用磁盤、

2022-08-26 00:21:44

IO模型線程

2024-08-08 14:57:32

2023-01-09 10:04:47

IO多路復(fù)用模型

2023-12-13 09:45:49

模型程序

2021-05-31 06:50:47

SelectPoll系統(tǒng)

2024-09-26 16:01:52

2023-05-08 00:06:45

Go語言機(jī)制

2009-06-29 18:09:12

多路復(fù)用Oracle

2022-07-11 08:02:15

KafkaSelector

2023-03-01 14:32:31

redisIOEpoll

2020-10-13 07:51:03

五種IO模型

2011-12-08 10:51:25

JavaNIO

2022-01-06 14:45:10

數(shù)據(jù)庫連接池IO

2023-12-06 07:16:31

Go語言語句

2025-04-24 10:05:51

2023-08-07 08:52:03

Java多路復(fù)用機(jī)制

2021-03-05 11:26:42

面試Java程序

2022-09-12 06:33:15

Select多路復(fù)用

2025-04-10 03:00:00

點(diǎn)贊
收藏

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