NIUCLOUD是一款SaaS管理后台框架多应用插件+云编译。上千名开发者、服务商正在积极拥抱开发者生态。欢迎开发者们免费入驻。一起助力发展! 广告
[TOC] ## select 函数原理 [select](https://pubs.opengroup.org/onlinepubs/9699919799/functions/select.html):select系统调用是用来让程序监视多个文件句柄的状态变化的。程序会停在 select 函数调用处等待,直到被监视的文件句柄有一个或多个发⽣生了状态改变。 ```c++ #include <algorithm> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> static const int MAX_LINK_NUM = 4096; static const int LISTEN_SOCKET = 0; // 保存新请求的文件句柄 static void save_fd(const char* ip, int new_sock, int* fd) { for (int i = 1; i < MAX_LINK_NUM; ++i) { if (fd[i] < 0) { fd[i] = new_sock; printf("get a new client from %s, save in fd[%d]\n", ip, i); return; } } printf("get a new client from %s, failed to save", ip); close(new_sock); } static void worker(const fd_set& rfds, const fd_set& wfds, int* fd) { for (int i = 0; i < MAX_LINK_NUM; ++i) { if (LISTEN_SOCKET == i && FD_ISSET(fd[i], &rfds)) { // 发生新的请求 struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(fd[LISTEN_SOCKET], (struct sockaddr*)&client, &len); if (new_sock < 0) { perror("fail to accept new client"); continue; } else { save_fd(inet_ntoa(client.sin_addr), new_sock, fd); } } else if (LISTEN_SOCKET != i && FD_ISSET(fd[i], &rfds)) { static const int MAX_READ_SIZE = 1024; char buffer[MAX_READ_SIZE]; ssize_t s = read(fd[i], buffer, sizeof(buffer) - 1); if (s > 0) { // 读取数据 buffer[s] = '\0'; printf("client message[%d]: %s\n", i, buffer); if (FD_ISSET(fd[i], &wfds)) { const char* msg = "HTTP/1.0 200 OK\n"; write(fd[i], msg, strlen(msg)); } } else if (0 == s) { printf("fd[%d] quit\n", i); close(fd[i]); fd[i] = -1; } else { perror("failed to read fd"); close(fd[i]); fd[i] = -1; } } } } static void server(int listensock) { int fd[MAX_LINK_NUM]; memset(fd, -1, sizeof(fd)); fd[LISTEN_SOCKET] = listensock; fd_set rfds; fd_set wfds; while (true) { int maxfd = 0; FD_ZERO(&rfds); FD_ZERO(&wfds); for (int i = 0;i < MAX_LINK_NUM; ++i) { if (fd[i] > 0) { FD_SET(fd[i], &rfds); FD_SET(fd[i], &wfds); maxfd = std::max(fd[i], maxfd); } } int ret = select(maxfd + 1, &rfds, &wfds, NULL, NULL); if (ret == 0) { perror("select is timeout"); break; } else if (ret == -1) { perror("failed to select"); break; } else { worker(rfds, wfds, fd); } } } ``` ## poll 函数原理 [poll](https://pubs.opengroup.org/onlinepubs/9699919799/functions/poll.html):poll采用一个 pollfd 指针向内核传递需要关心的描述符及其相关事件。 ```c++ #include <algorithm> #include <arpa/inet.h> #include <poll.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> static const int MAX_LINK_NUM = 4096; static const int LISTEN_SOCKET = 0; static bool save_fd(const char* ip, int new_sock, struct pollfd* events) { for (int i = 0; i < MAX_LINK_NUM; ++i) { if (events[i].fd < 0) { events[i].fd = new_sock; printf("get a new client from %s, save in fd[%d]\n", ip, i); return true; } } printf("get a new client from %s, failed to save", ip); close(new_sock); return false; } static int worker(struct pollfd* events, int nfds) { for (int i = 0; i < MAX_LINK_NUM; ++i) { if (LISTEN_SOCKET == i && events[i].revents & POLLIN) { // 发生新的请求 struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(events[LISTEN_SOCKET].fd, (struct sockaddr*)&client, &len); if (new_sock < 0) { perror("fail to accept new client"); continue; } else { if (save_fd(inet_ntoa(client.sin_addr), new_sock, events)) { ++nfds; } else { printf("failed to save new client\n"); } } } else if (LISTEN_SOCKET != i && events[i].revents & POLLIN) { static const int MAX_READ_SIZE = 1024; char buffer[MAX_READ_SIZE]; ssize_t s = read(events[i].fd, buffer, sizeof(buffer) - 1); if (s > 0) { // 读取数据 buffer[s] = '\0'; printf("client message[%d]: %s\n", i, buffer); if (events[i].revents & POLLOUT) { const char* msg = "HTTP/1.0 200 OK\n"; write(events[i].fd, msg, strlen(msg)); } } else if (0 == s) { printf("fd[%d] quit\n", i); close(events[i].fd); events[i].fd = -1; --nfds; } else { perror("failed to read fd"); close(events[i].fd); events[i].fd = -1; --nfds; } } } return nfds; } static void server(int listensock) { struct pollfd events[MAX_LINK_NUM]; for (int i = 0; i < MAX_LINK_NUM; ++i) { events[i].fd = -1; } int nfds = 1; events[LISTEN_SOCKET].fd = listensock; events[LISTEN_SOCKET].events = POLLIN; while (true) { int ret = poll(events, nfds, -1); if (ret == 0) { perror("poll is timeout"); break; } else if (ret == -1) { perror("failed to poll"); break; } else { nfds = worker(events, nfds); } } } ``` ## epoll 函数原理 epoll 函数是select和poll函数的增强版本,显著减少程序在大量并发连接中只有少量活跃的情况下CPU利用率,该函数不会复用文件描述符集合来传递结果,而迫使开发者每次等待事件之前都必须重新设置要等待的文件描述符集合,且获取事件时无需遍历整个文件描述符集合,只需要遍历被内核异步唤醒加入ready队列的描述符集合。 * `epoll_create`:创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大; * `epoll_ctl`:epoll的事件注册函数; * `epoll_wait`:等待事件的产生; ```c++ #include <algorithm> #include <arpa/inet.h> #include <sys/epoll.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> static const int MAX_LINK_NUM = 4096; // 创建socket并监控指定ip和端口 static int create_socket(const char* ip, int port) { int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建socket if (sock < 0) { perror("failed to create socket"); exit(1); } sockaddr_in local; // 设置监听端口配置 local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = inet_addr(ip); // 将socket绑定到端口上 if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) { perror("failed to bind socket"); exit(1); } // socket开始监听端口 static int BACK_LOG = 10; if(listen(sock, BACK_LOG) < 0) { perror("failed to listen socket"); exit(1); } return sock; } static void worker(int epollfd, int listensock, struct epoll_event* events, int num) { for (int i = 0; i < num; ++i) { if (events[i].data.fd == listensock && events[i].events & EPOLLIN) { // 发生新的请求 struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(events[i].data.fd, (struct sockaddr*)&client, &len); if (new_sock < 0) { perror("fail to accept new client"); continue; } else { struct epoll_event event; event.data.fd = new_sock; event.events = EPOLLIN; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, new_sock, &event) < 0) { printf("failed to save new client\n"); } } } else if (events[i].data.fd != listensock && events[i].events & EPOLLIN) { static const int MAX_READ_SIZE = 1024; char buffer[MAX_READ_SIZE]; ssize_t s = read(events[i].data.fd, buffer, sizeof(buffer) - 1); if (s > 0) { // 读取数据 buffer[s] = '\0'; printf("client message[%d]: %s\n", events[i].data.fd, buffer); struct epoll_event event; event.data.fd = events[i].data.fd; event.events = EPOLLOUT; epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &event); } else if (0 == s) { printf("fd[%d] quit\n", events[i].data.fd); close(events[i].data.fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); } else { perror("failed to read fd"); close(events[i].data.fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); } } else if (events[i].data.fd != listensock && events[i].events & EPOLLOUT) { const char* msg = "HTTP/1.0 200 OK\n"; write(events[i].data.fd, msg, strlen(msg)); struct epoll_event event; event.data.fd = events[i].data.fd; event.events = EPOLLIN; epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &event); } } } static void server(int listensock) { int epollfd = epoll_create(MAX_LINK_NUM); if (epollfd < 0) { perror("failed to create epollfd"); return; } struct epoll_event event; event.events = EPOLLIN; event.data.fd = listensock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &event) < 0) { perror("failed to add event"); return; } struct epoll_event events[MAX_LINK_NUM]; while (true) { int ret = epoll_wait(epollfd, events, MAX_LINK_NUM, -1); if (ret == 0) { perror("epoll is timeout"); break; } else if (ret == -1) { perror("failed to epoll"); break; } else { worker(epollfd, listensock, events, ret); } } } ``` ## select、poll、epoll 对比 | 系统调用 | select | poll | epoll | | --- | --- | --- | --- | | 事件集合 | 用户通过3个参数,分别传入感兴趣的可读、可写和异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。用户每次调用select都需要重置三个参数。 | 统一处理事件,只需要一个事件集参数,用户通过pollfd.events 传入感兴趣的事件,内核通过修改events来反馈其中就绪的事件。 | 内核通过一个事件表直接管理用户感兴趣的所有事件,类似poll。 | | 应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) | | 最大支持文件描述符数 | 一般为1024 | 65535 | 65535 | | 工作模式 | LT | LT | 支持ET高效模式,默认为ET模式 | | 内核实现和工作效率 | 采用轮询的方式来检测就绪事件,复杂度O(n) | 采用轮询的方式来检测就绪事件,复杂度O(n) | 采用回调方式来检测就绪事件,复杂度O(1) | 如果是连接数量不是特别多,但是经常会有连接加入或者退出的时候,就要考虑`poll`或者`select`了。 epoll 相比 select 效率更高,主要是基于其操作系统支持的I/O事件通知机制,而select是基于轮询机制。 **LT:水平触发**,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。 **ET:边缘触发**,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。