<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【面試】徹底理解 IO多路復(fù)用

          共 13985字,需瀏覽 28分鐘

           ·

          2021-05-05 08:00

          目錄

          1、什么是IO多路復(fù)用?
          2、為什么出現(xiàn)IO多路復(fù)用機(jī)制?
          3、IO多路復(fù)用的三種實現(xiàn)方式
          4、select函數(shù)接口
          5、select使用示例
          6、select缺點
          7、poll函數(shù)接口
          8、poll使用示例
          9、poll缺點
          10、epoll函數(shù)接口
          11、epoll使用示例
          12、epoll缺點
          13、epoll LT 與 ET模式的區(qū)別
          14、epoll應(yīng)用
          15、select/poll/epoll之間的區(qū)別
          16、IO多路復(fù)用完整代碼實現(xiàn)
          17、高頻面試題

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

          「定義」

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

          2、為什么有IO多路復(fù)用機(jī)制?

          沒有IO多路復(fù)用機(jī)制時,有BIO、NIO兩種實現(xiàn)方式,但有一些問題

          同步阻塞(BIO)

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

          // 偽代碼描述
          while(1) {
            // accept阻塞
            client_fd = accept(listen_fd)
            fds.append(client_fd)
            for (fd in fds) {
              // recv阻塞(會影響上面的accept)
              if (recv(fd)) {
                // logic
              }
            }  
          }
          • 服務(wù)器端采用多線程,當(dāng)accept一個請求后,開啟線程進(jìn)行recv,可以完成并發(fā)處理,但隨著請求數(shù)增加需要增加系統(tǒng)線程,大量的線程占用很大的內(nèi)存空間,并且線程切換會帶來很大的開銷,10000個線程真正發(fā)生讀寫事件的線程數(shù)不會超過20%,每次accept都開一個線程也是一種資源浪費

          // 偽代碼描述
          while(1) {
            // accept阻塞
            client_fd = accept(listen_fd)
            // 開啟線程read數(shù)據(jù)(fd增多導(dǎo)致線程數(shù)增多)
            new Thread func() {
              // recv阻塞(多線程不影響上面的accept)
              if (recv(fd)) {
                // logic
              }
            }  
          }

          同步非阻塞(NIO)

          • 服務(wù)器端當(dāng)accept一個請求后,加入fds集合,每次輪詢一遍fds集合recv(非阻塞)數(shù)據(jù),沒有數(shù)據(jù)則立即返回錯誤,每次輪詢所有fd(包括沒有發(fā)生讀寫事件的fd)會很浪費cpu

          setNonblocking(listen_fd)
          // 偽代碼描述
          while(1) {
            // accept非阻塞(cpu一直忙輪詢)
            client_fd = accept(listen_fd)
            if (client_fd != null) {
              // 有人連接
              fds.append(client_fd)
            } else {
              // 無人連接
            }  
            for (fd in fds) {
              // recv非阻塞
              setNonblocking(client_fd)
              // recv 為非阻塞命令
              if (len = recv(fd) && len > 0) {
                // 有讀寫數(shù)據(jù)
                // logic
              } else {
                 無讀寫數(shù)據(jù)
              }
            }  
          }

          IO多路復(fù)用(現(xiàn)在的做法)

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

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

          3、IO多路復(fù)用的三種實現(xiàn)方式

          • select

          • poll

          • epoll

          4、select函數(shù)接口

          #include <sys/select.h>
          #include <sys/time.h>

          #define FD_SETSIZE 1024
          #define NFDBITS (8 * sizeof(unsigned long))
          #define __FDSET_LONGS (FD_SETSIZE/NFDBITS)

          // 數(shù)據(jù)結(jié)構(gòu) (bitmap)
          typedef struct {
              unsigned long fds_bits[__FDSET_LONGS];
          } fd_set;

          // API
          int select(
              int max_fd, 
              fd_set *readset, 
              fd_set *writeset, 
              fd_set *exceptset, 
              struct timeval *timeout
          )                              // 返回值就緒描述符的數(shù)目

          FD_ZERO(int fd, fd_set* fds)   // 清空集合
          FD_SET(int fd, fd_set* fds)    // 將給定的描述符加入集合
          FD_ISSET(int fd, fd_set* fds)  // 判斷指定描述符是否在集合中 
          FD_CLR(int fd, fd_set* fds)    // 將給定的描述符從文件中刪除 
           

          5、select使用示例

          int main() {
            /*
             * 這里進(jìn)行一些初始化的設(shè)置,
             * 包括socket建立,地址的設(shè)置等,
             */

            fd_set read_fs, write_fs;
            struct timeval timeout;
            int max = 0;  // 用于記錄最大的fd,在輪詢中時刻更新即可

            // 初始化比特位
            FD_ZERO(&read_fs);
            FD_ZERO(&write_fs);

            int nfds = 0; // 記錄就緒的事件,可以減少遍歷的次數(shù)
            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) {
                   --nfds;
                   // 這里處理accept事件
                   FD_SET(i, &read_fd);//將客戶端socket加入到集合中
                }
                if (FD_ISSET(i, &read_fd)) {
                  --nfds;
                  // 這里處理read事件
                }
                if (FD_ISSET(i, &write_fd)) {
                   --nfds;
                  // 這里處理write事件
                }
              }
            }

          6、select缺點

          • 單個進(jìn)程所打開的FD是有限制的,通過FD_SETSIZE設(shè)置,默認(rèn)1024

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

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

          7、poll函數(shù)接口

          poll與select相比,只是沒有fd的限制,其它基本一樣

          #include <poll.h>
          // 數(shù)據(jù)結(jié)構(gòu)
          struct pollfd {
              int fd;                         // 需要監(jiān)視的文件描述符
              short events;                   // 需要內(nèi)核監(jiān)視的事件
              short revents;                  // 實際發(fā)生的事件
          };

          // API
          int poll(struct pollfd fds[], nfds_t nfds, int timeout);

          8、poll使用示例

          // 先宏定義長度
          #define MAX_POLLFD_LEN 4096  

          int main() {
            /*
             * 在這里進(jìn)行一些初始化的操作,
             * 比如初始化數(shù)據(jù)和socket等。
             */

            int nfds = 0;
            pollfd fds[MAX_POLLFD_LEN];
            memset(fds, 0, sizeof(fds));
            fds[0].fd = listenfd;
            fds[0].events = POLLRDNORM;
            int max  = 0;  // 隊列的實際長度,是一個隨時更新的,也可以自定義其他的
            int timeout = 0;

            int current_size = max;
            while (1) {
              // 阻塞獲取
              // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
              nfds = poll(fds, max+1, timeout);
              if (fds[0].revents & POLLRDNORM) {
                  // 這里處理accept事件
                  connfd = accept(listenfd);
                  //將新的描述符添加到讀描述符集合中
              }
              // 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生
              for (int i = 1; i < max; ++i) {     
                if (fds[i].revents & POLLRDNORM) { 
                   sockfd = fds[i].fd
                   if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
                      // 這里處理read事件
                      if (n == 0) {
                          close(sockfd);
                          fds[i].fd = -1;
                      }
                   } else {
                       // 這里處理write事件     
                   }
                   if (--nfds <= 0) {
                      break;       
                   }   
                }
              }
            }

          9、poll缺點

          • 每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大

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

          10、epoll函數(shù)接口

          #include <sys/epoll.h>

          // 數(shù)據(jù)結(jié)構(gòu)
          // 每一個epoll對象都有一個獨立的eventpoll結(jié)構(gòu)體
          // 用于存放通過epoll_ctl方法向epoll對象中添加進(jìn)來的事件
          // epoll_wait檢查是否有事件發(fā)生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可
          struct eventpoll {
              /*紅黑樹的根節(jié)點,這顆樹中存儲著所有添加到epoll中的需要監(jiān)控的事件*/
              struct rb_root  rbr;
              /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/
              struct list_head rdlist;
          };

          // API

          int epoll_create(int size); // 內(nèi)核中間加一個 ep 對象,把所有需要監(jiān)聽的 socket 都放到 ep 對象中
          int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 負(fù)責(zé)把 socket 增加、刪除到內(nèi)核紅黑樹
          int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 負(fù)責(zé)檢測可讀隊列,沒有可讀 socket 則阻塞進(jìn)程

          11、epoll使用示例

          int main(int argc, char* argv[])
          {
             /*
             * 在這里進(jìn)行一些初始化的操作,
             * 比如初始化數(shù)據(jù)和socket等。
             */

              // 內(nèi)核中創(chuàng)建ep對象
              epfd=epoll_create(256);
              // 需要監(jiān)聽的socket放到ep中
              epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
           
              while(1) {
                // 阻塞獲取
                nfds = epoll_wait(epfd,events,20,0);
                for(i=0;i<nfds;++i) {
                    if(events[i].data.fd==listenfd) {
                        // 這里處理accept事件
                        connfd = accept(listenfd);
                        // 接收新連接寫到內(nèi)核對象中
                        epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
                    } else if (events[i].events&EPOLLIN) {
                        // 這里處理read事件
                        read(sockfd, BUF, MAXLINE);
                        //讀完后準(zhǔn)備寫
                        epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
                    } else if(events[i].events&EPOLLOUT) {
                        // 這里處理write事件
                        write(sockfd, BUF, n);
                        //寫完后準(zhǔn)備讀
                        epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
                    }
                }
              }
              return 0;
          }

          12、epoll缺點

          • epoll只能工作在linux下

          13、epoll LT 與 ET模式的區(qū)別

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

          • LT模式下,只要這個fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作

          • ET模式下,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀完,或者遇到EAGAIN錯誤

          14、epoll應(yīng)用

          • redis

          • nginx

          15、select/poll/epoll之間的區(qū)別

          16、完整代碼示例

          https://github.com/caijinlin/learning-pratice/tree/master/linux/io

          17、高頻面試題

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

          • nginx/redis 所使用的IO模型是什么?

          • select、poll、epoll之間的區(qū)別

          • epoll 水平觸發(fā)(LT)與 邊緣觸發(fā)(ET)的區(qū)別?


          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號


          好文章,我在看??

          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99热精品在线 | 激情五月成人网 | 91成人高清 | 豆花视频在线免费观看 | 色逼资源站 |