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.
-
- nfds: The largest file descriptor among the three sets detected by the entrusted kernel + 1
- 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
- success:
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; }