Глубокое понимание мультиплексирования ввода-вывода

задняя часть
Глубокое понимание мультиплексирования ввода-вывода

Прочитав следующее, вы ответите на наиболее часто задаваемые вопросы интервью.

содержание

1. Что такое мультиплексирование ввода-вывода?
2. Почему появляется механизм мультиплексирования ввода-вывода?
3. Три реализации мультиплексирования ввода-вывода
4, выберите функциональный интерфейс
5. Выберите пример использования
6. Выберите недостатки
7, интерфейс функции опроса
8. Пример использования пула
9. Недостатки опроса
10. Интерфейс функции epoll
11. Пример использования epoll
12. Недостатки epoll
13. Разница между режимом epoll LT и ET
14, приложение epoll
15. Разница между select/poll/epoll
16, полная реализация кода мультиплексирования ввода-вывода
17. Часто задаваемые вопросы на собеседовании

1. Что такое мультиплексирование ввода-вывода

"определение"

  • М1 мультиплексирование является синхронной моделью IO, которая реализует нить, которая может контролировать несколько ручек файлов; как только дескриптор файла готов, он может уведомить приложение для выполнения соответствующих операций считывания и записи; когда файл файлов будет заблокирован, приложение будет заблокировано , передать процессор. Мультиплексирование относится к сетевым соединениям, а мультиплексирование относится к той же нити

2. Зачем нужен механизм мультиплексирования ввода-вывода?

Когда нет механизма мультиплексирования ввода-вывода, есть две реализации BIO и NIO, но есть некоторые проблемы

Синхронная блокировка (БИО)

  • Сервер принимает один поток, после принятия запроса, когда вызов recv или send заблокирован, он не сможет принимать другие запросы (необходимо дождаться recv или send предыдущего запроса),无法处理并发
// 伪代码描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  fds.append(client_fd)
  for (fd in fds) {
    // recv阻塞(会影响上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}
  • На стороне сервера используется многопоточность.После принятия запроса открывается поток для recv для завершения параллельной обработки, но по мере увеличения количества запросов необходимо увеличивать системный поток.大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费
// 伪代码描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  // 开启线程read数据(fd增多导致线程数增多)
  new Thread func() {
    // recv阻塞(多线程不影响上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}

Синхронный неблокирующий (NIO)

  • Когда сервер принимает запрос, он присоединяется к коллекции fds, каждый раз опрашивает данные recv (неблокирующие) коллекции fds и немедленно возвращает ошибку, если данных нет.每次轮询所有fd(包括没有发生读写事件的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) {
      // 有读写数据
      // logic
    } else {
       无读写数据
    }
  }  
}

Мультиплексирование ввода-вывода (текущая практика)

  • Сторона сервера использует один поток для получения списка fd с помощью системных вызовов, таких как select/epoll, и проходит через fd с событиями accept/recv/send, чтобы он мог支持更多的并发连接请求
fds = [listen_fd]
// 伪代码描述
while(1) {
  // 通过内核获取有读写事件发生的fd,只要有一个则返回,无则阻塞
  // 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,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. Три реализации мультиплексирования ввода-вывода

  • select
  • poll
  • epoll

4, выберите функциональный интерфейс

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

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

// 数据结构 (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
)                              // 返回值就绪描述符的数目

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. Выберите пример использования

int main() {
  /*
   * 这里进行一些初始化的设置,
   * 包括socket建立,地址的设置等,
   */

  fd_set read_fs, write_fs;
  struct timeval timeout;
  int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可

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

  int nfds = 0; // 记录就绪的事件,可以减少遍历的次数
  while (1) {
    // 阻塞获取
    // 每次需要把fd从用户态拷贝到内核态
    nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
    // 每次需要遍历所有fd,判断有无读写事件发生
    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. Выберите недостатки

  • FD, открываемый одним процессом, ограничен, задается FD_SETSIZE, по умолчанию 1024.
  • Каждый раз, когда вы вызываете select, вам нужно копировать набор fd из пользовательского режима в режим ядра.Эти накладные расходы будут очень большими, когда имеется много fd.
  • При сканировании сокета это линейное сканирование, и используется метод опроса, который менее эффективен (при высоком параллелизме)

7, интерфейс функции опроса

По сравнению с select, poll не имеет ограничений по fd, а остальные в принципе такие же

#include <poll.h>
// 数据结构
struct pollfd {
    int fd;                         // 需要监视的文件描述符
    short events;                   // 需要内核监视的事件
    short revents;                  // 实际发生的事件
};

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

8. Пример использования пула

// 先宏定义长度
#define MAX_POLLFD_LEN 4096  

int main() {
  /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和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从用户态拷贝到内核态
    nfds = poll(fds, max+1, timeout);
    if (fds[0].revents & POLLRDNORM) {
        // 这里处理accept事件
        connfd = accept(listenfd);
        //将新的描述符添加到读描述符集合中
    }
    // 每次需要遍历所有fd,判断有无读写事件发生
    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. Недостатки опроса

  • Каждый раз, когда вызывается опрос, набор fd необходимо копировать из пользовательского состояния в состояние ядра.Эти накладные расходы будут очень большими при наличии большого количества fd.
  • При сканировании сокета это линейное сканирование, и используется метод опроса, который менее эффективен (при высоком параллелизме)

10. Интерфейс функции epoll

#include <sys/epoll.h>

// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

// API

int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程

11. Пример использования epoll

int main(int argc, char* argv[])
{
   /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和socket等。
   */

    // 内核中创建ep对象
    epfd=epoll_create(256);
    // 需要监听的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);
              // 接收新连接写到内核对象中
              epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
          } else if (events[i].events&EPOLLIN) {
              // 这里处理read事件
              read(sockfd, BUF, MAXLINE);
              //读完后准备写
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
          } else if(events[i].events&EPOLLOUT) {
              // 这里处理write事件
              write(sockfd, BUF, n);
              //写完后准备读
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
          }
      }
    }
    return 0;
}

12. Недостатки epoll

  • epoll может работать только под linux

13. Разница между режимом epoll LT и ET

  • epoll имеет два режима запуска, EPOLLLT и EPOLLET, LT — режим по умолчанию, а ET — «высокоскоростной» режим.
  • В режиме LT, пока у fd есть данные для чтения, каждый epoll_wait будет возвращать свое событие, напоминая пользователю о необходимости работы с программой.
  • В режиме ET он будет запрашиваться только один раз до тех пор, пока он не будет запрашиваться перед потоком данных в следующий раз, независимо от того, есть ли данные для чтения в FD. Таким образом, в режиме ET вы должны закончить свой буфер, когда у вас есть FD, иначе вы столкнетесь с ошибкой EAGAIN.

14. приложение epoll

  • redis
  • nginx

15. Разница между select/poll/epoll

select poll epoll
структура данных bitmap множество красно-черное дерево
Максимальное количество подключений 1024 неограниченный неограниченный
копия Каждый вызов для выбора копии Каждый вызов для копирования опроса fd вызывает epoll_ctl copy в первый раз и не копирует каждый раз, когда вызывается epoll_wait
Эффективность работы Опрос: O(n) Опрос: O(n) Обратный вызов: O(1)

16. Полный пример кода

пример кода[1]

17. Часто задаваемые вопросы на собеседовании

  • Что такое мультиплексирование ввода-вывода?
  • Какая модель ввода-вывода используется nginx/redis?
  • Разница между выбором, опросом, epoll
  • В чем разница между триггером уровня epoll (LT) и триггером края (ET)?

использованная литература

[1]

Пример кода:https://github.com/caijinlin/learning-pratice/tree/master/linux/io