IO multiplexing select/epoll model

Overview

  • The I/O model used by most programs (traditional blocking I/O model) is that a single process only performs I/O operations on one file descriptor at a time, and each I/O system call blocks until the data is completed. transmission.
  • However, some scenarios require checking whether I/O operations are available on a file descriptor in a non-blocking manner. Check multiple file descriptors simultaneously to see if any of them can perform I/O operations. The corresponding solution is to use I/O multiplexing technology.
  • The goal of I/O multiplexing is to check the status of multiple file descriptors at the same time to see whether the I/O system call can be executed non-blockingly. The transition to the ready state of a file descriptor is triggered by some I/O events, and the operation of checking multiple file descriptors at the same time does not perform actual I/O operations. It just tells the process that a certain file descriptor is already in In the ready state, other system calls need to be called to complete the actual I/O operations.

select interface

  • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout);
    • Function role : multiplexing through select
    • parameter
      • nfds: The largest file descriptor among the three sets detected by the entrusted kernel + 1
        • The kernel needs to linearly traverse the file descriptors in these sets. This value is the condition for the end of the loop.
        • This parameter is invalid in Windows, just specify -1.
      • readfds: A collection of file descriptors. The kernel only detects the read buffer corresponding to the file descriptor in this collection.
        • Incoming and outgoing parameters, reading collections generally need to be detected, so as to know which file descriptor is used to receive data.
      • writefds: A collection of file descriptors. The kernel only detects the write buffer corresponding to the file descriptor in this collection.
        • Pass in and out parameters. If you do not need to use this parameter, you can specify it as NULL.
      • exceptfds: A collection of file descriptors. The kernel detects whether the file descriptors in the collection have abnormal status.
        • Pass in and out parameters. If you do not need to use this parameter, you can specify it as NULL.
      • timeout: Timeout duration, used to forcefully unblock the select() function.
        •   struct timeval {
                      
                      
            	    time_t      tv_sec;         /* seconds */
            	    suseconds_t tv_usec;        /* microseconds */
            };
          
        • NULL: The function cannot detect a ready file descriptor and will block forever.
        • Wait for a fixed length of time (seconds): The function cannot detect a ready file descriptor and will be forced to unblock after the specified length of time. The function returns 0
        • No waiting: The function will not block, just initialize the structure corresponding to the parameter to 0.
  • void FD_CLR(int fd, fd_set *set);
    • Function : Delete the file descriptor fd from the set collection (set the flag corresponding to fd to 0)
  • int FD_ISSET(int fd, fd_set *set);
    • Function : Determine whether the file descriptor fd is in the set collection (read whether the flag corresponding to fd is 0 or 1)
  • void FD_SET(int fd, fd_set *set);
    • Function : Add file descriptor fd to the set collection (set the flag corresponding to fd to 1)
  • void FD_ZERO(fd_set *set);
    • Function : Set the flag bits corresponding to all file descriptors in the set collection to 0. No file descriptors are added to the set.

epoll interface

  • int epoll_create(int size);

    • Function : Create an instance of the red-black tree model to manage the collection of file descriptors to be detected.
    • parameter
      • size: The number of file descriptors to create. After Linux kernel version 2.6.8, this parameter is ignored. You only need to specify a number greater than 0.
    • return value
      • Failure: return -1
      • Success: Returns a valid file descriptor through which the created epoll instance can be accessed.
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • Function : Manage nodes on the red-black tree instance, and can add, delete, and modify nodes.
    • parameter
      • epfd: File descriptor, parameter returned by epoll_create
      • op: Enumeration value, enumeration value, controls what operations are performed through this function
        • EPOLL_CTL_ADD: Add new nodes to the epoll model
        • EPOLL_CTL_MOD: Modify existing nodes in the epoll model
        • EPOLL_CTL_DEL: Delete the specified node in the epoll model
      • fd: File descriptor, that is, the file descriptor to be added/modified/deleted
      • event: epoll event, used to modify the file descriptor corresponding to the third parameter, specifying what event to detect this file descriptor
        •   // 联合体, 多个变量共用同一块内存        
            typedef union epoll_data {
                      
                      
             	void        *ptr;
            	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
            	uint32_t     u32;
            	uint64_t     u64;
            } epoll_data_t;
            
            struct epoll_event {
                      
                      
            	uint32_t     events;      /* Epoll events */
            	epoll_data_t data;        /* User data variable */
            };
          
        • events: Delegate events detected by epoll
          • EPOLLIN: Read event, receive data, detect read buffer, if there is data, the file descriptor is ready
          • EPOLLOUT: Write event, send data, detect write buffer, if the file descriptor is writable and ready
          • EPOLLERR: Abnormal event
        • data: User data variable, which is a union type. Usually, the fd member inside is used to store the value of the file descriptor to be detected. This value will be passed out when the epoll_wait() function is called.
    • return value
      • Failure: return -1
      • Success: return 0
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    • Function : Check whether there is a ready file descriptor in the created epoll instance
    • Function parameters
      • epfd: The return value of the epoll_create() function, find the epoll instance through this parameter
      • events: Outgoing parameter, which is the address of a structure array, which stores information about ready file descriptors.
      • maxevents: Modify the second parameter, the capacity of the structure array
      • timeout: If there is no ready file descriptor in the detected epoll instance, the length of time this function blocks, in milliseconds
        • 0: The function does not block. Regardless of whether there is a ready file descriptor in the epoll instance, the function will return directly after being called.
        • Greater than 0: If there is no ready file descriptor in the epoll instance, the function blocks for the corresponding number of milliseconds and then returns
        • -1: The function blocks until there is a ready file descriptor in the epoll instance.
    • function return value
      • success:
        • Equal to 0: The function is blocked and is forcibly released, and no file descriptor that meets the conditions is detected.
        • Greater than 0: The total number of detected ready file descriptors
      • Failure: return -1

epoll working mode

  • epoll has two working modes: horizontal mode and edge mode
  • Level Triggered :
    • Default working mode.
    • When epoll_wait detects the occurrence of a descriptor event and notifies the application of this event, the application does not need to process it immediately. The next time epoll_wait is called, the application will respond again and notify the event.
    • For example, the client sends 1000 bytes of data, but the server only reads 500 bytes. The next time epoll_wait is called, the read event will continue to be triggered, and we can continue to read the remaining data.
  • Edge mode (Edge-Triggered) :
    • When epoll_wait detects a descriptor event and notifies the application of this event, the application must handle the event immediately. If not handled, this event will not be notified the next time epoll_event is called. Therefore, in this mode, data needs to be received in a loop until all data is received.
    • For example, the client sends 1000 bytes of data, but the server only reads 500 bytes. The read event will not be triggered the next time epoll_wait is called.
  • The edge trigger mode greatly reduces the number of times the epoll event is triggered, and is more efficient than the horizontal trigger mode. In edge-triggered mode, a non-blocking interface must be used.

select code

  •   #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <string.h>
      #include <arpa/inet.h>
      
      int main()
      {
          
          
          // 1. 创建监听的fd
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
      
          // 2. 绑定
          struct sockaddr_in addr;
          addr.sin_family = AF_INET;
          addr.sin_port = htons(9999);
          addr.sin_addr.s_addr = INADDR_ANY;
          bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
      
          // 3. 设置监听
          listen(lfd, 128);
      
          // 将监听的fd的状态检测委托给内核检测
          int maxfd = lfd;
          // 初始化检测的读集合
          fd_set rdset;
          fd_set rdtemp;
          // 清零
          FD_ZERO(&rdset);
          // 将监听的lfd设置到检测的读集合中
          FD_SET(lfd, &rdset);
          // 通过select委托内核检测读集合中的文件描述符状态, 检测read缓冲区有没有数据
          // 如果有数据, select解除阻塞返回
          // 应该让内核持续检测
          while(1)
          {
          
          
              // 默认阻塞
              // rdset 中是委托内核检测的所有的文件描述符
              rdtemp = rdset;
              int num = select(maxfd+1, &rdtemp, NULL, NULL, NULL);
              // rdset中的数据被内核改写了, 只保留了发生变化的文件描述的标志位上的1, 没变化的改为0
              // 只要rdset中的fd对应的标志位为1 -> 缓冲区有数据了
              // 判断
              // 有没有新连接
              if(FD_ISSET(lfd, &rdtemp))
              {
          
          
                  // 接受连接请求, 这个调用不阻塞
                  struct sockaddr_in cliaddr;
                  int cliLen = sizeof(cliaddr);
                  int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &cliLen);
      
                  // 得到了有效的文件描述符
                  // 通信的文件描述符添加到读集合
                  // 在下一轮select检测的时候, 就能得到缓冲区的状态
                  FD_SET(cfd, &rdset);
                  // 重置最大的文件描述符
                  maxfd = cfd > maxfd ? cfd : maxfd;
              }
      
              // 没有新连接, 通信
              for(int i=0; i<maxfd+1; ++i)
              {
          
          
      			// 判断从监听的文件描述符之后到maxfd这个范围内的文件描述符是否读缓冲区有数据
                  if(i != lfd && FD_ISSET(i, &rdtemp))
                  {
          
          
                      // 接收数据
                      char buf[10] = {
          
          0};
                      // 一次只能接收10个字节, 客户端一次发送100个字节
                      // 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据
                      // 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次
                      // 	循环会一直持续, 知道缓冲区数据被读完位置
                      int len = read(i, buf, sizeof(buf));
                      if(len == 0)
                      {
          
          
                          printf("客户端关闭了连接...\n");
                          // 将检测的文件描述符从读集合中删除
                          FD_CLR(i, &rdset);
                          close(i);
                      }
                      else if(len > 0)
                      {
          
          
                          // 收到了数据
                          // 发送数据
                          write(i, buf, strlen(buf)+1);
                      }
                      else
                      {
          
          
                          // 异常
                          perror("read");
                      }
                  }
              }
          }
      
          return 0;
      }
    

epoll code

horizontal trigger mode

  •   #include <stdio.h>
      #include <ctype.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <string.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      #include <sys/epoll.h>
      
      // server
      int main(int argc, const char* argv[])
      {
          
          
      		
          // 创建监听的套接字
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
          if(lfd == -1)
          {
          
          
              perror("socket error");
              exit(1);
          }
      
          // 绑定
          struct sockaddr_in serv_addr;
          memset(&serv_addr, 0, sizeof(serv_addr));
          serv_addr.sin_family = AF_INET;
          serv_addr.sin_port = htons(9999);
          serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本地多有的IP
          
          // 设置端口复用
          int opt = 1;
          setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      
          // 绑定端口
          int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
          if(ret == -1)
          {
          
          
              perror("bind error");
              exit(1);
          }
      
          // 监听
          ret = listen(lfd, 64);
          if(ret == -1)
          {
          
          
              perror("listen error");
              exit(1);
          }
      
          // 现在只有监听的文件描述符
          // 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll
          // 创建一个epoll模型
          int epfd = epoll_create(100);
          if(epfd == -1)
          {
          
          
              perror("epoll_create");
              exit(0);
          }
      
          // 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
          struct epoll_event ev;
          ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
          ev.data.fd = lfd;
          ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
          if(ret == -1)
          {
          
          
              perror("epoll_ctl");
              exit(0);
          }
      
          struct epoll_event evs[1024];
          int size = sizeof(evs) / sizeof(struct epoll_event);
          // 持续检测
          while(1)
          {
          
          
              // 调用一次, 检测一次
              int num = epoll_wait(epfd, evs, size, -1);
              for(int i=0; i<num; ++i)
              {
          
          
                  // 取出当前的文件描述符
                  int curfd = evs[i].data.fd;
                  // 判断这个文件描述符是不是用于监听的
                  if(curfd == lfd)
                  {
          
          
                      // 建立新的连接
                      int cfd = accept(curfd, NULL, NULL);
                      // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
                      ev.events = EPOLLIN;    // 读缓冲区是否有数据
                      ev.data.fd = cfd;
                      ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                      if(ret == -1)
                      {
          
          
                          perror("epoll_ctl-accept");
                          exit(0);
                      }
                  }
                  else
                  {
          
          
                      // 处理通信的文件描述符
                      // 接收数据
                      char buf[5] = {
          
          0};
                      int len = recv(curfd, buf, sizeof(buf), 0);
                      if(len == 0)
                      {
          
          
                          printf("客户端已经断开了连接\n");
                          // 将这个文件描述符从epoll模型中删除
                          epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                          close(curfd);
                      }
                      else if(len > 0)
                      {
          
          
                          printf("客户端say: %s\n", buf);
                          send(curfd, buf, len, 0);
                      }
                      else
                      {
          
          
                          perror("recv");
                          exit(0);
                      } 
                  }
              }
          }
      
          return 0;
      }
    

edge triggered mode

  •   #include <stdio.h>
      #include <ctype.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <string.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      #include <sys/epoll.h>
      #include <fcntl.h>
      #include <errno.h>
      
      // server
      int main(int argc, const char* argv[])
      {
          
          
          // 创建监听的套接字
          int lfd = socket(AF_INET, SOCK_STREAM, 0);
          if(lfd == -1)
          {
          
          
              perror("socket error");
              exit(1);
          }
      
          // 绑定
          struct sockaddr_in serv_addr;
          memset(&serv_addr, 0, sizeof(serv_addr));
          serv_addr.sin_family = AF_INET;
          serv_addr.sin_port = htons(9999);
          serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本地多有的IP
          // 127.0.0.1
          // inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
          
          // 设置端口复用
          int opt = 1;
          setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      
          // 绑定端口
          int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
          if(ret == -1)
          {
          
          
              perror("bind error");
              exit(1);
          }
      
          // 监听
          ret = listen(lfd, 64);
          if(ret == -1)
          {
          
          
              perror("listen error");
              exit(1);
          }
      
          // 现在只有监听的文件描述符
          // 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll
          // 创建一个epoll模型
          int epfd = epoll_create(100);
          if(epfd == -1)
          {
          
          
              perror("epoll_create");
              exit(0);
          }
      
          // 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
          struct epoll_event ev;
          ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
          ev.data.fd = lfd;
          ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
          if(ret == -1)
          {
          
          
              perror("epoll_ctl");
              exit(0);
          }
      
      
          struct epoll_event evs[1024];
          int size = sizeof(evs) / sizeof(struct epoll_event);
          // 持续检测
          while(1)
          {
          
          
              // 调用一次, 检测一次
              int num = epoll_wait(epfd, evs, size, -1);
              printf("==== num: %d\n", num);
      
              for(int i=0; i<num; ++i)
              {
          
          
                  // 取出当前的文件描述符
                  int curfd = evs[i].data.fd;
                  // 判断这个文件描述符是不是用于监听的
                  if(curfd == lfd)
                  {
          
          
                      // 建立新的连接
                      int cfd = accept(curfd, NULL, NULL);
                      // 将文件描述符设置为非阻塞
                      // 得到文件描述符的属性
                      int flag = fcntl(cfd, F_GETFL);
                      flag |= O_NONBLOCK;
                      fcntl(cfd, F_SETFL, flag);
                      // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
                      // 通信的文件描述符检测读缓冲区数据的时候设置为边沿模式
                      ev.events = EPOLLIN | EPOLLET;    // 读缓冲区是否有数据
                      ev.data.fd = cfd;
                      ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                      if(ret == -1)
                      {
          
          
                          perror("epoll_ctl-accept");
                          exit(0);
                      }
                  }
                  else
                  {
          
          
                      // 处理通信的文件描述符
                      // 接收数据
                      char buf[5] = {
          
          0};
                      // 循环读数据
                      while(1)
                      {
          
          
                          int len = recv(curfd, buf, sizeof(buf), 0);
                          if(len == 0)
                          {
          
          
                              // 非阻塞模式下和阻塞模式是一样的 => 判断对方是否断开连接
                              printf("客户端断开了连接...\n");
                              // 将这个文件描述符从epoll模型中删除
                              epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                              close(curfd);
                              break;
                          }
                          else if(len > 0)
                          {
          
          
                              // 通信
                              // 接收的数据打印到终端
                              write(STDOUT_FILENO, buf, len);
                              // 发送数据
                              send(curfd, buf, len, 0);
                          }
                          else
                          {
          
          
                              // len == -1
                              if(errno == EAGAIN)
                              {
          
          
                                  printf("数据读完了...\n");
                                  break;
                              }
                              else
                              {
          
          
                                  perror("recv");
                                  exit(0);
                              }
                          }
                      }
                  }
              }
          }
      
          return 0;
      }
    

reference

Guess you like

Origin blog.csdn.net/new9232/article/details/135423250