主頁 >  其他 > 手把手寫C++服務器(31):服務器性能提升關鍵——IO復用技術【兩萬字長文】

手把手寫C++服務器(31):服務器性能提升關鍵——IO復用技術【兩萬字長文】

2021-09-02 08:12:29 其他

本系列文章導航: 手把手寫C++服務器(0):專欄文章-匯總導航【更新中】

前言: Linux中素有“萬物皆檔案,一切皆IO”的說法,前面幾講手撕了CGI網關服務器、echo回顯服務器、discard服務的代碼,但是這幾個一次只能監聽一個檔案描述符,因此性能非常原始、低下,IO復用能使服務器同時監聽多個檔案描述符,是服務器性能提升的關鍵,雖然IO復用本身是阻塞的,但是和并發技術結合起來,再加上一點設計模式,一個高性能服務器的基石就基本搭建完成了,

目錄

1、預備知識

(1)檔案描述符

(2)行程阻塞

(3)快取IO

(4)什么是IO多路復用?

2、Linux五大IO模型

(1)阻塞IO

(2)非阻塞IO

(3)IO多路復用

(4)信號驅動IO

(5)異步IO

3、select

函式回傳

引數詳解

重要結構體詳解

使用流程

代碼實體

4、poll

函式原型

重要結構體詳解

事件型別

使用流程

代碼實體

5、epoll

函式原型

函式回傳

LT水平觸發模式和ET邊沿觸發模式

代碼實體

6、三組IO復用函式對比

1. 用戶態將檔案描述符傳入內核的方式

2. 內核態檢測檔案描述符讀寫狀態的方式

3. 找到就緒的檔案描述符并傳遞給用戶態的方式

4. 重復監聽的處理方式

7、經典面試題:epoll更高效的原因

寫在最后

參考


1、預備知識

(1)檔案描述符

強烈推薦看一下本系列的第25講《手把手寫C++服務器(25):萬物皆可檔案之socket fd》

檔案描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向檔案的參考的抽象化概念, 檔案描述符在形式上是一個非負整數,實際上,它是一個索引值,指向內核為每一個行程所維護的該行程打開檔案的記錄表,當程式打開一個現有檔案或者創建一個新檔案時,內核向行程回傳一個檔案描述符,在程式設計中,一些涉及底層的程式撰寫往往會圍繞著檔案描述符展開,但是檔案描述符這一概念往往只適用于UNIX、Linux這樣的作業系統,

(2)行程阻塞

正在執行的行程,由于期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新作業做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態,可見,行程的阻塞是行程自身的一種主動行為,也因此只有處于運行態的行程(獲得了CPU資源),才可能將其轉為阻塞狀態,當行程進入阻塞狀態,是不占用CPU資源的,

(3)快取IO

快取I/O又稱為標準I/O,大多數檔案系統的默認I/O操作都是快取I/O,在Linux的快取I/O機制中,作業系統會將I/O的資料快取在檔案系統的頁快取中,即資料會先被拷貝到作業系統內核的緩沖區中,然后才會從作業系統內核的緩沖區拷貝到應用程式的地址空間,

快取 I/O 的缺點:

資料在傳輸程序中需要在應用程式地址空間和內核進行多次資料拷貝操作,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的,

(4)什么是IO多路復用?

IO 多路復用是一種同步IO模型,實作一個執行緒可以監視多個檔案句柄;一旦某個檔案句柄就緒,就能夠通知應用程式進行相應的讀寫操作;沒有檔案句柄就緒就會阻塞應用程式,交出CPU,

2、Linux五大IO模型

(1)阻塞IO

這是最常用的簡單的IO模型,阻塞IO意味著當我們發起一次IO操作后一直等待成功或失敗之后才回傳,在這期間程式不能做其它的事情,阻塞IO操作只能對單個檔案描述符進行操作,詳見read或write,

(2)非阻塞IO

我們在發起IO時,通過對檔案描述符設定O_NONBLOCK flag來指定該檔案描述符的IO操作為非阻塞,非阻塞IO通常發生在一個for回圈當中,因為每次進行IO操作時要么IO操作成功,要么當IO操作會阻塞時回傳錯誤EWOULDBLOCK/EAGAIN,然后再根據需要進行下一次的for回圈操作,這種類似輪詢的方式會浪費很多不必要的CPU資源,是一種糟糕的設計,和阻塞IO一樣,非阻塞IO也是通過呼叫read或write來進行操作的,也只能對單個描述符進行操作,

(3)IO多路復用

IO多路復用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是類似的,但具體細節各有不同:首先都會對一組檔案描述符進行相關事件的注冊,然后阻塞等待某些事件的發生或等待超時,IO多路復用都可以關注多個檔案描述符,但對于這三種機制而言,不同數量級檔案描述符對性能的影響是不同的,下面會詳細介紹,

(4)信號驅動IO

信號驅動IO是利用信號機制,讓內核告知應用程式檔案描述符的相關事件,

但信號驅動IO在網路編程的時候通常很少用到,因為在網路環境中,和socket相關的讀寫事件太多了,比如下面的事件都會導致SIGIO信號的產生:

  1. TCP連接建立
  2. 一方斷開TCP連接請求
  3. 斷開TCP連接請求完成
  4. TCP連接半關閉
  5. 資料到達TCP socket
  6. 資料已經發送出去(如:寫buffer有空余空間)

上面所有的這些都會產生SIGIO信號,但我們沒辦法在SIGIO對應的信號處理函式中區分上述不同的事件,SIGIO只應該在IO事件單一情況下使用,比如說用來監聽埠的socket,因為只有客戶端發起新連接的時候才會產生SIGIO信號,

(5)異步IO

異步IO和信號驅動IO差不多,但它比信號驅動IO可以多做一步:相比信號驅動IO需要在程式中完成資料從用戶態到內核態(或反方向)的拷貝,異步IO可以把拷貝這一步也幫我們完成之后才通知應用程式,我們使用 aio_read 來讀,aio_write 寫,

同步IO vs 異步IO

1. 同步IO指的是程式會一直阻塞到IO操作如read、write完成

2. 異步IO指的是IO操作不會阻塞當前程式的繼續執行
所以根據這個定義,上面阻塞IO當然算是同步的IO,非阻塞IO也是同步IO,因為當檔案運算子可用時我們還是需要阻塞的讀或寫,同理IO多路復用和信號驅動IO也是同步IO,只有異步IO是完全完成了資料的拷貝之后才通知程式進行處理,沒有阻塞的資料讀寫程序,

3、select

select的作用是在一段指定的時間內,監聽用戶感興趣的檔案描述符上的可讀、可寫、例外等事件,函式原型如下:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                fd_set *exceptfds, struct timeval *timeout);

函式回傳

  • select成功時回傳就緒檔案描述符的總數;
  • 如果在超時時間內沒有任何檔案描述符就緒,select將回傳0;
  • select失敗時回傳-1并設定errno,;
  • 如果在select等待期間,程式接收到信號,select立即回傳-1,并將errno設定為EINTR,

引數詳解

  • nfds:指定被監聽檔案描述符總數,通常被設定為select監聽所有檔案描述符中的最大值+1,
  • readfds:可讀事件對應檔案描述符集合,
  • writefds:可寫事件對應檔案描述符集合,
  • exceptfds:例外事件對應檔案描述符集合,
  • timeout:設定select超時時間,

重要結構體詳解

readfds、writefds、exceptfds都是fd_set結構體,timeout是timeval結構體,這里詳解一下這兩個結構體,

1、fd_set

fd_set結構體定義比較復雜,涉及到位操作,比較復雜,所以通常用宏來訪問fd_set中的位,

#include <sys/select.h>
FD_ZERO(fd_set* fdset);    // 清除fdset中的所有位
FD_SET(int fd, fd_set* fdset); // 設定fdset中的位
FD_CLR(int fd, fd_set* fdset); // 清除fdset中的位
int FD_ISSET(int fd, fd_set* fdset);  // 測驗fdset的位fd是否被設定
  • FD_ZERO用來清空檔案描述符組,每次呼叫select前都需要清空一次,
  • FD_SET添加一個檔案描述符到組中,FD_CLR對應將一個檔案描述符移出組中,
  • FD_ISSET檢測一個檔案描述符是否在組中,我們用這個來檢測一次select呼叫之后有哪些檔案描述符可以進行IO操作,

2、timeval

struct timeval {
    long tv_sec; // 秒數
    long tv_usec; // 微妙數
};

使用流程

綜上所述,我們一般的使用流程是:

  1. 準備作業——定義readfds、timeval等
  2. 使用FD_ZERO清零,使用FD_SET設定檔案描述符,因為事件發生后,檔案描述符集合都將被內核修改,
  3. 呼叫select
  4. 使用FD_ISSET檢測檔案描述符是否在組中

代碼實體

根據使用流程,給出一個代碼示例:

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TIMEOUT 5 /* select timeout in seconds */
#define BUF_LEN 1024 /* read buffer in bytes */

int main (void) {
  struct timeval tv;
  fd_set readfds;
  int ret;
  
  /* Wait on stdin for input. */
  FD_ZERO(&readfds);
  FD_SET(STDIN_FILENO, &readfds);
  /* Wait up to five seconds. */
  tv.tv_sec = TIMEOUT;
  tv.tv_usec = 0;
  
  /* All right, now block! */
  ret = select (STDIN_FILENO + 1, &readfds,
                NULL,
                NULL, 
                &tv);
  if (ret == ?1) {
    perror ("select");
    return 1; 
  } else if (!ret) {
    printf ("%d seconds elapsed.\n", TIMEOUT);
    return 0; 
  }
  /*
  * Is our file descriptor ready to read?
  * (It must be, as it was the only fd that
  * we provided and the call returned
  * nonzero, but we will humor ourselves.)
  */
  if (FD_ISSET(STDIN_FILENO, &readfds)) {
    char buf[BUF_LEN+1];
    int len;
    /* guaranteed to not block */
    len = read (STDIN_FILENO, buf, BUF_LEN);
    if (len == ?1) {
      perror ("read");
      return 1; 
    }
    if (len) {
      buf[len] = '\0';
      printf ("read: %s\n", buf);
    }
    return 0; 
  }

  fprintf (stderr, "This should not happen!\n");
  return 1; 
}

后面一講會給出一些實用的例子,有了select之后我們可以同時監聽很多個請求,系統的處理能力大大增強了,

4、poll

和select類似,在一定時間內輪詢一定數量的檔案描述符,

函式原型

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

但是和select不同的是,select需要用三組檔案描述符,poll只有一個pollfd檔案陣列,陣列中的每個元素都表示一個需要監聽IO操作事件的檔案描述符,而且我們只需要關心陣列中events引數,revents由內核自動填充,

重要結構體詳解

    struct pollfd {
        int fd;    // 檔案描述符
        short events;    // 注冊的事件 
        short revents;   // 實際發生的事件,由內核填充
    };

事件型別

具體的事件型別參看手冊:https://man7.org/linux/man-pages/man2/poll.2.html

       POLLIN There is data to read.

       POLLPRI
              There is some exceptional condition on the file
              descriptor.  Possibilities include:

              ? There is out-of-band data on a TCP socket (see tcp(7)).

              ? A pseudoterminal master in packet mode has seen a state
                change on the slave (see ioctl_tty(2)).

              ? A cgroup.events file has been modified (see cgroups(7)).

       POLLOUT
              Writing is now possible, though a write larger than the
              available space in a socket or pipe will still block
              (unless O_NONBLOCK is set).

       POLLRDHUP (since Linux 2.6.17)
              Stream socket peer closed connection, or shut down writing
              half of connection.  The _GNU_SOURCE feature test macro
              must be defined (before including any header files) in
              order to obtain this definition.

       POLLERR
              Error condition (only returned in revents; ignored in
              events).  This bit is also set for a file descriptor
              referring to the write end of a pipe when the read end has
              been closed.

       POLLHUP
              Hang up (only returned in revents; ignored in events).
              Note that when reading from a channel such as a pipe or a
              stream socket, this event merely indicates that the peer
              closed its end of the channel.  Subsequent reads from the
              channel will return 0 (end of file) only after all
              outstanding data in the channel has been consumed.

       POLLNVAL
              Invalid request: fd not open (only returned in revents;
              ignored in events).

       When compiling with _XOPEN_SOURCE defined, one also has the
       following, which convey no further information beyond the bits
       listed above:

       POLLRDNORM
              Equivalent to POLLIN.

       POLLRDBAND
              Priority band data can be read (generally unused on
              Linux).

       POLLWRNORM
              Equivalent to POLLOUT.

       POLLWRBAND
              Priority data may be written.

使用流程

綜上所述,我們一般的使用流程是:

  1. 定義pollfd陣列,并設定poll陣列相關引數,
  2. 設定超時時間
  3. 呼叫poll

代碼實體

根據使用流程,給出一個代碼示例:

#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#define TIMEOUT 5 /* poll timeout, in seconds */
int main (void) {
  struct pollfd fds[2];
  int ret;
  /* watch stdin for input */
  fds[0].fd = STDIN_FILENO;
  fds[0].events = POLLIN;
  /* watch stdout for ability to write (almost always true) */
  fds[1].fd = STDOUT_FILENO;
  fds[1].events = POLLOUT;
  /* All set, block! */
  ret = poll (fds, 2, TIMEOUT * 1000);
  if (ret == ?1) {
    perror ("poll");
    return 1; 
  }
  if (!ret) {
    printf ("%d seconds elapsed.\n", TIMEOUT);
    return 0; 
  }

  if (fds[0].revents & POLLIN)
    printf ("stdin is readable\n");
  if (fds[1].revents & POLLOUT)
    printf ("stdout is writable\n");
  return 0; 
}

5、epoll

epoll是Linux特有的IO復用函式,使用一組函式來完成任務,而不是單個函式,

epoll把用戶關心的檔案描述符上的事件放在內核的一個事件表中,不需要像select、poll那樣每次呼叫都要重復傳入檔案描述符集或事件集,

epoll需要使用一個額外的檔案描述符,來唯一標識內核中的時間表,由epoll_create創建,

函式原型

    #include <sys/epoll.h>

    int epoll_create(int size);
    int epoll_create1(int flags);

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

    int epoll_wait(int epfd, struct epoll_event *events,
                int maxevents, int timeout);
    int epoll_pwait(int epfd, struct epoll_event *events,
                int maxevents, int timeout,
                const sigset_t *sigmask);
  • epoll_create:創建一個epoll實體,size引數給內核一個提示,標識事件表的大小,函式回傳的檔案描述符將作用其他所有epoll系統呼叫的第一個引數,以指定要訪問的內核事件表,
  • epoll_ctl:操作檔案描述符,fd表示要操作的檔案描述符,op指定操作型別,event指定事件,
  • epoll_wait:在一段超時時間內等待一組檔案描述符上的事件,如果監測到事件,就將所有就緒的事件從內核事件表(epfd引數指定)中復制到第二個引數events指向的陣列中,因為events陣列只用于輸出epoll_wait監測到的就緒事件,而不像select、poll那樣就用于傳入用戶注冊的事件,又用于輸出內核檢測到的就緒事件,這樣極大提高了應用程式索引就緒檔案描述符的效率,

函式回傳

特別注意epoll_wait函式成功時回傳就緒的檔案描述符總數,select和poll回傳檔案描述符總數,

以尋找已經就緒的檔案描述符,舉個例子如下:

epoll_wait只需要遍歷回傳的檔案描述符,但是poll和select需要遍歷所有檔案描述符

//  poll
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
// 必須遍歷所有已注冊的檔案描述符
for (int i = 0; i < MAX_EVENT_NUMBER; i++) {
    if (fds[i].revents & POLLIN) {
        int sockfd = fds[i].fd;
    }
}

// epoll_wait
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
// 僅需要遍歷就緒的ret個檔案描述符
for (int i = 0; i < ret; i++) {
    int sockfd = events[i].data.fd;
}

LT水平觸發模式和ET邊沿觸發模式

epoll監控多個檔案描述符的I/O事件,epoll支持邊緣觸發(edge trigger,ET)或水平觸發(level trigger,LT),通過epoll_wait等待I/O事件,如果當前沒有可用的事件則阻塞呼叫執行緒,

select和poll只支持LT作業模式,epoll的默認的作業模式是LT模式,

水平觸發:

  • 當epoll_wait檢測到其上有事件發生并將此事件通知應用程式后,應用程式可以不立即處理此事件,這樣應用程式下一次呼叫epoll_wait的時候,epoll_wait還會再次向應用程式通告此事件,直到事件被處理,

邊沿觸發:

  • 當epoll_wait檢測到其上有事件發生并將此事件通知應用程式后,應用程式必須立即處理此事件,后續的epoll_wait呼叫將不再向應用程式通知這一事件,

所以,邊沿觸發模式很大程度上降低了同一個epoll事件被重復觸發的次數,所以效率更高

代碼實體

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>

#define MAXEVENTS 64

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;
}

static int create_and_bind (char *port)
{
  struct addrinfo hints;
  struct addrinfo *result, *rp;
  int s, sfd;

  memset (&hints, 0, sizeof (struct addrinfo));
  hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */
  hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
  hints.ai_flags = AI_PASSIVE;     /* All interfaces */

  s = getaddrinfo (NULL, port, &hints, &result);
  if (s != 0)
    {
      fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
      return -1;
    }

  for (rp = result; rp != NULL; rp = rp->ai_next)
    {
      sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
      if (sfd == -1)
        continue;

      s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
      if (s == 0)
        {
          /* We managed to bind successfully! */
          break;
        }

      close (sfd);
    }

  if (rp == NULL)
    {
      fprintf (stderr, "Could not bind\n");
      return -1;
    }

  freeaddrinfo (result);

  return sfd;
}

int main (int argc, char *argv[])
{
  int sfd, s;
  int efd;
  struct epoll_event event;
  struct epoll_event *events;

  if (argc != 2)
    {
      fprintf (stderr, "Usage: %s [port]\n", argv[0]);
      exit (EXIT_FAILURE);
    }

  sfd = create_and_bind (argv[1]);
  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_create1 (0);
  if (efd == -1)
    {
      perror ("epoll_create");
      abort ();
    }

  event.data.fd = sfd;
  event.events = EPOLLIN | EPOLLET;
  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);

  /* The event loop */
  while (1)
    {
      int n, i;

      n = epoll_wait (efd, events, MAXEVENTS, -1);
      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\n");
	      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. */
              while (1)
                {
                  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)
                    {
                      if ((errno == EAGAIN) ||
                          (errno == EWOULDBLOCK))
                        {
                          /* We have processed all incoming
                             connections. */
                          break;
                        }
                      else
                        {
                          perror ("accept");
                          break;
                        }
                    }

                  s = getnameinfo (&in_addr, in_len,
                                   hbuf, sizeof hbuf,
                                   sbuf, sizeof sbuf,
                                   NI_NUMERICHOST | NI_NUMERICSERV);
                  if (s == 0)
                    {
                      printf("Accepted connection on descriptor %d "
                             "(host=%s, port=%s)\n", infd, hbuf, sbuf);
                    }

                  /* Make the incoming socket non-blocking and add it to the
                     list of fds to monitor. */
                  s = make_socket_non_blocking (infd);
                  if (s == -1)
                    abort ();

                  event.data.fd = infd;
                  event.events = EPOLLIN | EPOLLET;
                  s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
                  if (s == -1)
                    {
                      perror ("epoll_ctl");
                      abort ();
                    }
                }
              continue;
            }
          else
            {
              /* We have data on the fd waiting to be read. Read and
                 display it. We must read whatever data is available
                 completely, as we are running in edge-triggered mode
                 and won't get a notification again for the same
                 data. */
              int done = 0;

              while (1)
                {
                  ssize_t count;
                  char buf[512];

                  count = read (events[i].data.fd, buf, sizeof buf);
                  if (count == -1)
                    {
                      /* If errno == EAGAIN, that means we have read all
                         data. So go back to the main loop. */
                      if (errno != EAGAIN)
                        {
                          perror ("read");
                          done = 1;
                        }
                      break;
                    }
                  else if (count == 0)
                    {
                      /* End of file. The remote has closed the
                         connection. */
                      done = 1;
                      break;
                    }

                  /* Write the buffer to standard output */
                  s = write (1, buf, count);
                  if (s == -1)
                    {
                      perror ("write");
                      abort ();
                    }
                }

              if (done)
                {
                  printf ("Closed connection on descriptor %d\n",
                          events[i].data.fd);

                  /* Closing the descriptor will make epoll remove it
                     from the set of descriptors which are monitored. */
                  close (events[i].data.fd);
                }
            }
        }
    }

  free (events);

  close (sfd);

  return EXIT_SUCCESS;
}

6、三組IO復用函式對比

1. 用戶態將檔案描述符傳入內核的方式

  • select:創建3個檔案描述符集并拷貝到內核中,分別監聽讀、寫、例外動作,這里受到單個行程可以打開的fd數量限制,默認是1024,
  • poll:將傳入的struct pollfd結構體陣列拷貝到內核中進行監聽,
  • epoll:執行epoll_create會在內核的高速cache區中建立一顆紅黑樹以及就緒鏈表(該鏈表存盤已經就緒的檔案描述符),接著用戶執行的epoll_ctl函式添加檔案描述符會在紅黑樹上增加相應的結點,

2. 內核態檢測檔案描述符讀寫狀態的方式

  • select:采用輪詢方式,遍歷所有fd,最后回傳一個描述符讀寫操作是否就緒的mask掩碼,根據這個掩碼給fd_set賦值,
  • poll:同樣采用輪詢方式,查詢每個fd的狀態,如果就緒則在等待佇列中加入一項并繼續遍歷,
  • epoll:采用回呼機制,在執行epoll_ctl的add操作時,不僅將檔案描述符放到紅黑樹上,而且也注冊了回呼函式,內核在檢測到某檔案描述符可讀/可寫時會呼叫回呼函式,該回呼函式將檔案描述符放在就緒鏈表中,

3. 找到就緒的檔案描述符并傳遞給用戶態的方式

  • select:將之前傳入的fd_set拷貝傳出到用戶態并回傳就緒的檔案描述符總數,用戶態并不知道是哪些檔案描述符處于就緒態,需要遍歷來判斷,
  • poll:將之前傳入的fd陣列拷貝傳出用戶態并回傳就緒的檔案描述符總數,用戶態并不知道是哪些檔案描述符處于就緒態,需要遍歷來判斷,
  • epoll:epoll_wait只用觀察就緒鏈表中有無資料即可,最后將鏈表的資料回傳給陣列并回傳就緒的數量,內核將就緒的檔案描述符放在傳入的陣列中,所以只用遍歷依次處理即可,

4. 重復監聽的處理方式

  • select:將新的監聽檔案描述符集合拷貝傳入內核中,繼續以上步驟,
  • poll:將新的struct pollfd結構體陣列拷貝傳入內核中,繼續以上步驟,
  • epoll:無需重新構建紅黑樹,直接沿用已存在的即可,

7、經典面試題:epoll更高效的原因?

select和poll的動作基本一致,只是poll采用鏈表來進行檔案描述符的存盤,而select采用fd標注位來存放,所以select會受到最大連接數的限制,而poll不會,

select、poll、epoll雖然都會回傳就緒的檔案描述符數量,但是select和poll并不會明確指出是哪些檔案描述符就緒,而epoll會,造成的區別就是,系統呼叫回傳后,呼叫select和poll的程式需要遍歷監聽的整個檔案描述符找到是誰處于就緒,而epoll則直接處理即可,

select、poll都需要將有關檔案描述符的資料結構拷貝進內核,最后再拷貝出來,而epoll創建的有關檔案描述符的資料結構本身就存于內核態中,

select、poll采用輪詢的方式來檢查檔案描述符是否處于就緒態,而epoll采用回呼機制,造成的結果就是,隨著fd的增加,select和poll的效率會線性降低,而epoll不會受到太大影響,除非活躍的socket很多,

epoll的邊緣觸發模式效率高,系統不會充斥大量不關心的就緒檔案描述符,

雖然epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函式回呼,

寫在最后

這一講偏理論,主要講了Linux中三種IO復用,后面幾講會在這一講的基礎上,圍繞IO寫一些有趣的實戰demo,敬請期待,

參考

  • https://blog.csdn.net/weixin_42145502/article/details/107320539?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163011698816780262548239%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163011698816780262548239&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-107320539.pc_v2_rank_blog_default&utm_term=IO%E5%A4%8D%E7%94%A8&spm=1018.2226.3001.4450
  • 《Linux高性能服務器編程》
  • https://juejin.cn/post/6882984260672847879
  • https://zhuanlan.zhihu.com/p/115220699
  • https://man7.org/linux/man-pages/man2/poll.2.html
  • https://zhuanlan.zhihu.com/p/159135478

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296880.html

標籤:其他

上一篇:C++list類模擬實作

下一篇:莫讓“學院派”限制我們的思維:在陣列的中間位置洗掉資料一定比鏈表慢?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more