当前位置: 首页 > news >正文

Linux应用开发-18- select、poll、epoll

同步和异步 I/O

  1. Synchronous在同步文件 I/O 中,线程启动 I/O 操作,并立即进入等待状态,直到 I/O 请求完成。

  2. Asynchronous执行异步文件 I/O 的线程通过调用相应的函数将 I/O 请求发送到内核。
    如果内核接受请求,调用线程将继续处理另一个作业,直到内核向线程发出 I/O 操作完成的信号。 然后,它会中断其当前作业,并在必要时处理
    I/O 操作中的数据。

    在这里插入图片描述

阻塞 I/O非阻塞 I/O

  1. 阻塞 I/O:在 IO 执行的两个阶段(用户空间与内核空间)都被阻塞了。read()/recvfrom()。

在这里插入图片描述

  1. 非阻塞 I/O :用户进程需要不断的 主动询问内核空间的数据准备好了没有。如果内核还未将素材准备好, 环境调用仍然会直接返回, 并且返回EWOULDBLOCK 错误码。read()/recvfrom()

加粗样式

多路复用 I/O

单个进程就可以同时处理 多个网络连接的 I/O,select、poll、epoll 等函数(操作系统内核处理,进程不用耗费资源去轮询)会不断的 轮询它们所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程。
在这里插入图片描述
select (最早的Posix标准)
它请内核“监视”三组文件描述符(FD):readfds(想读的), writefds(想写的), exceptfds(关心异常的)。

#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_ZERO(fd_set *set);              // 清空set
void FD_SET(int fd, fd_set *set);       // 将fd加入set
void FD_CLR(int fd, fd_set *set);       // 将fd从set中移除
int  FD_ISSET(int fd, fd_set *set);     // 测试fd是否在set中
/*
FD数量限制:fd_set 是一个位图,其大小由 FD_SETSIZE 宏限制(通常是1024)。你无法用它监视超过1024个连接。
两次遍历(O(n)):内核要遍历一次,用户进程也要遍历一次,效率随FD数量线性下降。
两次拷贝:每次调用都要在用户/内核空间拷贝 fd_set。
*/

poll (select的改进版)
poll 解决了 select 的 “FD数量限制” 问题。再使用位图 fd_set,而是使用一个struct pollfd 的数组。

struct pollfd {int   fd;         // 文件描述符short events;     // 请求的事件(POLLIN, POLLOUT)short revents;    // 返回的事件
};#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
依然需要在内核和用户空间拷贝整个数组,并且内核和用户依然需要遍历(O(n)这个数组。
*/

epoll (Linux最新)

epoll 的操作模式:LT(level trigger)和 ET(edge trigger)。LT 模式:即水平出发模式,当 epoll_wait 检测到 socket 描述符处于就绪时就通知应用程序,应用程序可以不立即处理它。下次调用 epoll_wait 时,还会再次产生通知。ET 模式:即边缘触发模式,当 epoll_wait 检测到 socket 描述符处于就绪时就通知应用程序,应用程序 必须立即处理它。如果不处理,下次调用 epoll_wait 时,不会再次产生通知。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

epoll工作分为三步:

  1. epoll_create:创建一个 epoll 实例,即这个函数返回一个指向这个“事件中心”的FD。
  2. epoll_ctl:管理”epoll 实例。内核会在这里注册一个回调函数。当这个 fd对应的设备(如网卡)真的收到数据并产生中断时,这个回调函数会被触发,它会把这个 fd 添加到一个“就绪列表”中。
  3. epoll_wait:用户进程的“等待”,如果“就绪列表”里有 fd,epoll_wait 会把这些 fd 拷贝到用户空间的events 数组中,然后立刻返回,进程被唤醒。

https://adrianwangs.github.io/2025/07/02/%E5%85%AB%E8%82%A1%E6%96%87/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BD%91%E7%BB%9C%E7%B3%BB%E7%BB%9F/IO%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/
来源

优点

  1. 它只关心“就绪列表”。如果1000万个连接中只有3个活跃,epoll_wait 就只返回这3个,开销极小。
  2. epoll_ctl 注册FD时,fd 和 event 信息就已经被内核*永久”保存(直到你DEL它)。你不需要在每次
    epoll_wait 时都把1000万个FD拷贝给内核。内核自己维护着“兴趣列表”(通常是红黑树)和“就D列表”(通常是双向链表)。
  3. mmap(内存映射):epoll 通过内核和用户空间共享一块内存(mmap)来传递“就绪列表”,避免了从内核空间到用户空间的不必要拷贝。

#include <sys/epoll.h>// 1. 创建实例
int epoll_create(int size); // size 在现代Linux中已无意义,但必须 > 0// 2. 管理FD
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
op
EPOLL_CTL_ADD:注册一个新的 fd 到 epoll 实例中(“请帮我监视这个fd”)。
EPOLL_CTL_MOD:修改一个已注册 fd 的监视事件。
EPOLL_CTL_DEL:删除一个 fd(“不用再监视它了”)。
*/// 3. 等待事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);// event 结构
typedef union epoll_data {void    *ptr;int      fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t     events;   // Epoll events (EPOLLIN, EPOLLOUT, EPOLLET...)epoll_data_t data;     // 用户数据 (通常存fd或一个指向自定义结构的指针)
};
#include <stdio.h>      // 包含标准输入输出函数 (如 printf, perror)
#include <stdlib.h>     // 包含通用工具函数 (如 exit)
#include <string.h>     // 包含字符串操作函数 (如 memset)
#include <unistd.h>     // 包含 POSIX 操作系统 API (如 close, read, write)
#include <sys/socket.h> // 包含 Socket 编程相关函数和结构体
#include <netinet/in.h> // 包含 IPv4 Internet 地址族相关结构体 (如 sockaddr_in)
#include <arpa/inet.h>  // 包含 IP 地址转换函数 (如 inet_ntoa)
#include <sys/epoll.h>  // ** 核心:包含 epoll 相关的函数和结构体 **
#include <fcntl.h>      // 包含文件控制函数 (如 fcntl),用于设置文件描述符标志 (如 O_NONBLOCK)
#include <errno.h>      // 包含错误码定义 (如 EAGAIN, EWOULDBLOCK)// 定义服务器监听端口
#define PORT 8080
// epoll_wait 一次调用最多可以返回的事件数量
#define MAX_EVENTS 10
// 读写操作的缓冲区大小
#define MAX_BUF 1024// 辅助函数:打印错误信息并退出程序
// 参数:
//   msg: 一个指向字符串的常量指针,用于描述发生错误的上下文信息。
// 作用:
//   调用 perror() 打印由 errno 全局变量设置的系统错误信息,
//   然后打印 msg,最后强制终止程序。
void die(const char* msg) {perror(msg);        // perror() 会根据当前的 errno 值打印一条错误消息到 stderrexit(EXIT_FAILURE); // exit() 终止程序,并返回 EXIT_FAILURE (通常是非零值) 表示失败
}// 辅助函数:设置文件描述符为非阻塞模式
// 参数:
//   fd: 需要设置为非阻塞的文件描述符 (int 类型)。
// 作用:
//   修改指定文件描述符的模式,使其进行 I/O 操作时,
//   如果操作不能立即完成 (例如没有数据可读或缓冲区已满),
//   则立即返回错误 (通常是 EAGAIN 或 EWOULDBLOCK),而不是阻塞程序。
void set_non_blocking(int fd) {// 获取当前文件描述符的标志// fcntl(fd, F_GETFL, 0)//   fd: 目标文件描述符。//   F_GETFL: 命令字,表示获取文件状态标志。//   0: 在 F_GETFL 命令下,第三个参数是忽略的。// 返回值: 成功时返回文件状态标志,失败时返回 -1。int flags = fcntl(fd, F_GETFL, 0);if (flags == -1) {die("fcntl F_GETFL failed"); // 如果获取失败,打印错误并退出}// 设置 O_NONBLOCK 标志// fcntl(fd, F_SETFL, flags | O_NONBLOCK)//   fd: 目标文件描述符。//   F_SETFL: 命令字,表示设置文件状态标志。//   flags | O_NONBLOCK: 新的文件状态标志。使用位或操作将 O_NONBLOCK 标志//     添加到已有的标志中,保留其他标志不变。O_NONBLOCK 表示非阻塞模式。// 返回值: 成功时返回 0,失败时返回 -1。if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {die("fcntl F_SETFL failed"); // 如果设置失败,打印错误并退出}
}int main() {int listener_fd;              // 用于监听传入连接的 Socket 文件描述符int conn_fd;                  // 成功接受的客户端连接的 Socket 文件描述符int epoll_fd;                 // epoll 实例的文件描述符 (由 epoll_create1 返回)int event_count;              // epoll_wait 调用返回的就绪事件的数量struct sockaddr_in serv_addr; // 存储服务器的 IPv4 地址和端口信息的结构体struct sockaddr_in client_addr; // 存储客户端的 IPv4 地址和端口信息的结构体socklen_t client_len = sizeof(client_addr); // client_addr 结构体的大小,用于 accept()struct epoll_event event;     // 用于向 epoll 实例添加/修改事件时使用的单个事件结构体struct epoll_event events[MAX_EVENTS]; // 数组,用于存储 epoll_wait 返回的就绪事件char buffer[MAX_BUF];         // 用于在 Socket 上读写数据的缓冲区// --- 1. 标准的网络服务器初始化设置 ---// 创建监听 Socket// socket(int domain, int type, int protocol)//   domain: 地址族,AF_INET 表示 IPv4。//   type: Socket 类型,SOCK_STREAM 表示 TCP 连接 (字节流)。//   protocol: 协议,0 表示根据 domain 和 type 自动选择默认协议 (TCP)。// 返回值: 成功时返回一个新的文件描述符,失败时返回 -1。listener_fd = socket(AF_INET, SOCK_STREAM, 0);if (listener_fd == -1) {die("socket creation failed");}// 设置 Socket 选项:允许地址复用// setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)//   sockfd: 目标 Socket 文件描述符 (listener_fd)。//   level: 选项级别,SOL_SOCKET 表示在 Socket 级别设置选项。//   optname: 选项名称,SO_REUSEADDR 允许重新绑定到最近关闭的端口。//   optval: 指向存储选项值的缓冲区的指针 (这里是 int opt = 1)。//   optlen: optval 缓冲区的大小。// 返回值: 成功时返回 0,失败时返回 -1。int opt = 1; // 1 表示启用该选项if (setsockopt(listener_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {die("setsockopt SO_REUSEADDR failed");}// 清零服务器地址结构体并设置其属性memset(&serv_addr, 0, sizeof(serv_addr)); // memset(void *s, int c, size_t n)// s: 指向要填充内存块的指针 (&serv_addr)。// c: 要设置的值 (这里是 0)。// n: 要设置的字节数 (这里是 sizeof(serv_addr))。serv_addr.sin_family = AF_INET;         // 地址族:IPv4serv_addr.sin_addr.s_addr = INADDR_ANY; // IP 地址:监听所有可用的网络接口 (0.0.0.0)// INADDR_ANY 是一个宏,表示 0.0.0.0,以网络字节序表示。serv_addr.sin_port = htons(PORT);       // 端口号:将主机字节序的 PORT 转换为网络字节序// 绑定 Socket 到指定的 IP 地址和端口// bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)//   sockfd: 要绑定的 Socket (listener_fd)。//   addr: 指向 sockaddr 结构体的指针,包含要绑定的地址信息 (&serv_addr)。//   addrlen: addr 结构体的大小 (sizeof(serv_addr))。// 返回值: 成功时返回 0,失败时返回 -1。if (bind(listener_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {die("socket bind failed");}// 将 Socket 设置为监听模式,等待客户端连接// listen(int sockfd, int backlog)//   sockfd: 监听 Socket (listener_fd)。//   backlog: 等待连接队列的最大长度。SOMAXCONN 是系统默认的最大值。// 返回值: 成功时返回 0,失败时返回 -1。if (listen(listener_fd, SOMAXCONN) == -1) {die("socket listen failed");}// --- 2. Epoll 实例创建和监听 Socket 注册 ---// ** 关键步骤 1: 创建 epoll 实例 **// epoll_create1(int flags)//   flags: 用于控制 epoll 实例行为的标志。0 表示默认行为。//          其他常见标志如 EPOLL_CLOEXEC (execve 时关闭此 fd)。// 返回值: 成功时返回一个新的 epoll 文件描述符,失败时返回 -1。epoll_fd = epoll_create1(0);if (epoll_fd == -1) {die("epoll_create1 failed");}// ** 关键步骤 2: 将监听 Socket (listener_fd) 注册到 epoll 实例 **// 设置 event 结构体:定义我们关心 listener_fd 上的什么事件// event.events: 这是一个位掩码,表示我们希望监听的事件类型。//   EPOLLIN: 表示文件描述符可读。对于监听 Socket,这意味着有新的连接请求。event.events = EPOLLIN;// event.data.fd: 这是一个 union 成员,我们将 listener_fd 存储在这里,//   以便在 epoll_wait 返回时能够识别是哪个文件描述符触发了事件。event.data.fd = listener_fd; // epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//   epfd: epoll 实例的文件描述符 (epoll_fd)。//   op: 操作类型。EPOLL_CTL_ADD 表示向 epoll 实例添加一个文件描述符。//   fd: 要操作的文件描述符 (listener_fd)。//   event: 指向 struct epoll_event 结构体的指针,定义了要监听的事件。// 返回值: 成功时返回 0,失败时返回 -1。if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listener_fd, &event) == -1) {die("epoll_ctl: add listener_fd failed");}printf("服务器启动,监听端口 %d...\n", PORT);// --- 3. Epoll 主事件循环 ---while (1) { // 服务器主循环,永不停止地等待和处理事件// ** 关键步骤 3: 等待就绪事件 **// epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)//   epfd: epoll 实例的文件描述符 (epoll_fd)。//   events: 指向一个 epoll_event 数组的指针,用于存储内核返回的就绪事件。//   maxevents: events 数组的最大容量 (MAX_EVENTS)。//   timeout: 等待超时时间,单位毫秒。-1 表示无限期阻塞直到有事件发生。//            0 表示立即返回,不阻塞。正数表示等待指定毫秒数。// 返回值: 成功时返回就绪事件的数量,超时时返回 0,失败时返回 -1。event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (event_count == -1) {die("epoll_wait failed");}// --- 4. 处理所有已发生的就绪事件 ---for (int i = 0; i < event_count; i++) { // 遍历所有返回的就绪事件// 检查当前事件是否来自监听 Socket (listener_fd)if (events[i].data.fd == listener_fd) {// 表示有新的客户端连接请求到达// 接受新连接// accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)//   sockfd: 监听 Socket (listener_fd)。//   addr: 指向 sockaddr 结构体的指针,用于存储客户端的地址信息。//   addrlen: 指向 addr 结构体大小的指针。函数返回时会更新为实际大小。// 返回值: 成功时返回一个新的连接 Socket 文件描述符 (conn_fd),失败时返回 -1。conn_fd = accept(listener_fd, (struct sockaddr*)&client_addr, &client_len);if (conn_fd == -1) {perror("accept failed"); // 打印 accept 错误,但服务器继续运行continue; // 跳过此事件,继续处理下一个事件}// ** 关键: 将新接受的连接 Socket (conn_fd) 设置为非阻塞模式 **// 这是为了确保后续对此 conn_fd 的 read/write 操作不会阻塞整个 epoll 循环。set_non_blocking(conn_fd);// ** 关键: 将新连接的 Socket (conn_fd) 注册到 epoll 实例 **// 设置 event 结构体:监听 EPOLLIN 事件 (表示客户端发送数据)event.events = EPOLLIN;event.data.fd = conn_fd; // 将新连接的 fd 关联到这个事件// 向 epoll 实例添加 conn_fdif (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &event) == -1) {die("epoll_ctl: add conn_fd failed");}printf("新连接来自 %s:%d, 分配 fd %d\n", inet_ntoa(client_addr.sin_addr), // inet_ntoa 将网络字节序的IP地址转换为点分十进制字符串ntohs(client_addr.sin_port),    // ntohs 将网络字节序的端口号转换为主机字节序conn_fd);} else {// 如果事件不是来自监听 Socket,那么它来自一个已连接的客户端 Socketint client_fd = events[i].data.fd; // 获取发生事件的客户端 fd// 尝试从客户端 Socket 读取数据// read(int fd, void *buf, size_t count)//   fd: 要读取的文件描述符 (client_fd)。//   buf: 指向存储读取数据的缓冲区的指针 (buffer)。//   count: 要读取的最大字节数 (MAX_BUF)。// 返回值: 成功时返回读取的字节数,0 表示到达文件末尾 (客户端关闭连接),-1 表示发生错误。ssize_t n = read(client_fd, buffer, MAX_BUF);if (n == 0) {// read 返回 0 表示客户端已经优雅地关闭了连接 (EOF)printf("客户端 fd %d 断开连接\n", client_fd);// 从 epoll 实例中移除此客户端 Socket// epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL)//   op: EPOLL_CTL_DEL 表示从 epoll 实例中删除一个文件描述符。//   event: 在删除操作时,此参数通常可以为 NULL。if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL) == -1) {perror("epoll_ctl: del client_fd failed");}close(client_fd); // 关闭客户端 Socket 文件描述符,释放资源} else if (n > 0) {// 成功读取到数据buffer[n] = '\0'; // 将读取到的数据作为C字符串结束,确保打印时不会乱码printf("收到来自 fd %d 的数据: %s", client_fd, buffer);// 将收到的数据回显给客户端// write(int fd, const void *buf, size_t count)//   fd: 要写入的文件描述符 (client_fd)。//   buf: 指向要写入数据的缓冲区的指针 (buffer)。//   count: 要写入的字节数 (n)。// 返回值: 成功时返回写入的字节数,-1 表示发生错误。if (write(client_fd, buffer, n) == -1) {perror("write to client failed");// 写入失败通常意味着连接有问题,可以考虑关闭此连接if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL) == -1) {perror("epoll_ctl: del client_fd after write error failed");}close(client_fd);}} else {// n < 0 表示读取发生错误// 检查错误类型是否是 EAGAIN 或 EWOULDBLOCKif (errno == EAGAIN || errno == EWOULDBLOCK) {// 这通常发生在非阻塞 Socket 上,表示当前没有数据可读// 在 epoll 的 LT 模式下,如果缓冲区未读空,下次 epoll_wait 还会通知。// 但在 ET 模式下,就需要在这里循环读直到收到 EAGAIN。printf("fd %d 发生 EAGAIN/EWOULDBLOCK (无数据可读)\n", client_fd);} else {// 其他类型的读取错误 (例如连接重置等)perror("read from client failed");// 发生错误,移除并关闭此 Socketif (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL) == -1) {perror("epoll_ctl: del client_fd after read error failed");}close(client_fd);}}} // end if-else (处理 listener_fd 或 client_fd 事件)} // end for (遍历所有就绪事件)} // end while(1) 主循环// 清理资源 (理论上服务器主循环永不退出,所以这部分代码不会被执行到)// close(int fd)//   fd: 要关闭的文件描述符。// 返回值: 成功时返回 0,失败时返回 -1。close(listener_fd); // 关闭监听 Socketclose(epoll_fd);    // 关闭 epoll 实例return 0;           // 程序成功退出 (虽然在实际服务器中通常不会走到这里)
}
http://www.dtcms.com/a/589119.html

相关文章:

  • 进程3:进程切换
  • PHP中各种超全局变量使用
  • 深入了解iOS内存管理
  • 介质电磁特性参数
  • 网站建设行业广告语建网站找那家企业好
  • Python中使用sqlite3模块和panel完成SQLite数据库中PDF的写入和读取
  • 佛山网站建设网络公司上海网站seo诊断
  • 操作系统面试题学习
  • Java 大视界 -- Java 大数据在智能教育虚拟学习环境构建与用户体验优化中的应用
  • .NET Core 如何使用 Quartz?
  • excel下拉选项设置
  • 深入解析:利用EBS直接API实现增量快照与精细化数据管理(AWS)
  • 专门做石材地花设计的网站有哪些网站是免费学做网页的
  • [Godot] Google Play审核反馈:如何应对“您的游戏需要进行更多测试才能发布正式版”?
  • Rust 练习册 :深入探索可变长度数量编码
  • dify二次开发部署服务器
  • webrtc降噪-NoiseEstimator类源码分析与算法原理
  • 4.3 Boost 库工具类 optional 的使用
  • 帮人做网站要怎么赚钱吗吉林平安建设网站
  • 文广网站建设sq网站推广
  • Nop平台拆分出核心部分nop-kernel
  • 结构型设计模式1
  • 普中51单片机学习笔记-中断
  • 二十六、STM32的ADC(DMA+ADC)
  • 网站开发的著作权和版权网站品牌推广
  • 【Docker】docker compose
  • 4.1.8 【2022 统考真题】
  • 深圳网站设计官网番禺人才网上
  • Tailwind CSS的Flex布局
  • 深入解析 LeetCode 1:两数之和