平台相关
select
- 支持:
windows
- 支持:
linux
,macos
,bsd
,aix
等
- 支持:
poll
- 支持:
linux
,macos
,bsd
等 - 不支持:一些交旧的或特定的
unix
操作系统可能不支持poll
- 不支持:
windows
不提供标准的poll
,但可以通过其他相似的api
来达到类似效果
- 支持:
epoll
- 支持:
linux
特有的
- 支持:
- 三方库
- 虽然
select
和poll
在大多数UNIX-like
系统中广泛支持,epoll
却仅限于Linux
- 要编写跨平台的网络代码,可以考虑使用库(如
libevent
或Boost.Asio
等)
这些库提供了对不同操作系统I/O
多路复用机制的统一封装
- 虽然
fd_set
1 2 3 |
typedef struct { // 实现特定的细节,可能是一个位数组 } fd_set; |
select
概述
select
允许应用程序同时监视多个文件描述符(例如套接字),以查看它们是否准备好进行读取、写入或是否有异常发生- 这种能力使得程序可以同时处理多个网络连接或其他
I/O
操作,而无需为每个连接使用单独的线程或进程
原型
1 2 3 4 5 |
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
参数
- 要监视的文件描述符数量(最大描述符值+1)
- 准备好读取的描述符集
- 准备好写入的描述符集
- 异常条件出现的描述符集
- 最长等待时间,或NULL以无限等待
返回值
- 正值
- 返回值表示在所提供的文件描述符集中准备好进行某些操作(读、写、异常)的文件描述符数量
- 也就是说表示有多少描述符具有待处理的事件
0
- 如果在指定的超时期间没有任何描述符准备好进行操作,
select
将返回0
- 这在某些情况下可能是正常的,但也可能是您的代码应该处理的特殊情况
- 如果在指定的超时期间没有任何描述符准备好进行操作,
- 负值
- 如果发生错误,
select
将返回-1
,并设置全局错误变量errno
以提供有关错误的更多信息
- 如果发生错误,
优点
- 简单易用
- 跨平台支持
缺点
- 描述符数量通常限制为1024
- 每次调用
select
时都必须重新初始化文件描述符集和超时结构 - 可能不适合处理大量并发连接
详细
- 初始化文件描述符集:
- 使用
FD_ZERO
和FD_SET
函数初始化要监视的文件描述符集合 - 通常,这些集合分别用于监视读操作、写操作和异常
- 使用
- 设置超时:
- 设置一个超时值,以指定
select
函数等待的最长时间
- 设置一个超时值,以指定
- 调用select函数:
- 检查结果:
- 返回后,读取、写入和异常的文件描述符集将被更新,以反映哪些描述符准备好进行相应的操作
- 可以使用
FD_ISSET
宏来检查特定描述符是否准备好
- 可以使用
- 返回后,读取、写入和异常的文件描述符集将被更新,以反映哪些描述符准备好进行相应的操作
- 处理准备好的描述符:
- 遍历准备好的描述符,并进行读取、写入或处理异常
- 重复:
- 通常,
select
调用将放在循环中,以持续监视描述符
- 通常,
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
fd_set master_set, read_set; int max_fd; struct timeval timeout; FD_ZERO(&master_set); FD_SET(server_socket, &master_set); max_fd = server_socket; while (1) { read_set = master_set; timeout.tv_sec = 5; timeout.tv_usec = 0; int ready_count = select(max_fd + 1, &read_set, NULL, NULL, &timeout); if (ready_count <= 0) continue; // 超时或错误 for (int i = 0; i <= max_fd; i++) { if (FD_ISSET(i, &read_set)) { if (i == server_socket) { // 接受新连接 } else { // 处理现有连接 } } } } |
select理解
描述符数量限制
- 在许多
UNIX
系统上,select
函数的文件描述符数量限制确实为1024
- 这个限制主要是因为在
select
的实现中,文件描述符集合通常使用位图表示,该位图的大小固定为FD_SETSIZE
,它通常定义为1024
定期检查(timeout
)?
select
函数不是定时检查所有描述符- 它是一种允许程序监视多个文件描述符(例如套接字)以查看它们是否准备好进行读取、写入或是否有异常条件的方法
- 调用
select
时,需要提供三个文件描述符集合(用于读取、写入和异常检测)以及一个可选的超时值(timeout
)
函数将阻塞,直到以下情况之一发生:- 描述符集合中的一个或多个描述符准备好进行读取、写入或有异常
- 指定的超时时间到达
- 被信号中断
timeout
参数理解
timeout
是一个“等待限制”,而不是监视描述符集合的时间- timeout设置为
NULL
select
会无限期地等待,直到至少有一个描述符准备好
- 超时时间到达,但没有描述符就绪
select
仍然会返回,但这是由于时间超时,而不是因为有描述符准备好- 这种情况下,可以检查返回的描述符集,看看它们是否为空,从而知道
select
是因为哪种原因返回的
- 在超时时间到达之前,有描述符就绪
- 这种情况下,
select
会立即返回,并告诉你哪些描述符已经准备好
- 这种情况下,
文件描述符位于?
- 文件描述符本身实际上是内核中资源的句柄,代表了一个打开的文件、套接字、管道等
- 这些描述符的状态和相关信息都是由内核管理的
- 调用
select
函数并传递一个或多个描述符集合时,内核会检查这些描述符的状态
内核单独记录文件描述符状态?
- 每个文件描述符都有一组与其关联的状态,这些状态用于跟踪文件描述符的不同属性,如是否可读、可写、是否存在异常条件等
- 这些状态并不是分开管理的,而是与文件描述符紧密关联的
select与内核的交互
- 调用
select
函数并传递readfds
参数时,实际上是在告诉内核您想要监视哪些特定的文件描述符是否可读- 因此,需要在调用
select
之前,手动设置readfds
中您关心的文件描述符 - 内核会检查我们关心的这些描述符的当前状态
- 因此,需要在调用
- 然后,
select
函数会阻塞,直到这些指定的文件描述符之一变为可读、超时发生,或者接收到一个信号- 这些情况下,
select
会返回,并且readfds
将被修改以指示哪些文件描述符现在是可读的 - 任何未准备好的文件描述符将在
readfds
集合中被清除
- 这些情况下,
select与客户端服务器程序
- 服务端
- 服务器首先创建一个监听套接字来等待客户端连接请求
该套接字的文件描述符通常会被加入到select
的readfds
集合中,以便服务器能知道何时有新的连接请求到来 - 一旦客户端连接被接受,它的新的文件描述符(代表特定的客户端连接)也会被加入到
readfds
(有时也可能是writefds
)集合中
以便服务器能读取该客户端发送的数据或向其发送数据
accept
成功时返回一个新的套接字描述符,该描述符代表与客户端的新连接
- 服务器首先创建一个监听套接字来等待客户端连接请求
- 客户端
- 客户端创建一个连接到服务器的套接字,这个套接字的文件描述符通常会被加入到客户端的
readfds
和/或writefds
集合中
以便客户端可以知道何时可以从服务器读取数据或向服务器写入数据
- 客户端创建一个连接到服务器的套接字,这个套接字的文件描述符通常会被加入到客户端的
关于select的返回值
select
函数的返回值表示整个集合中就绪的文件描述符的数量- 如果
readfds
里面有1
个就绪的描述符,而writefds
里面有2
个就绪的描述符,那么select
的返回值将会是3
- 如果
需要重新设置文件描述符
- 每次
select
调用返回后,只有那些就绪的文件描述符会保留在集合中,而其他的描述符则会被清除 - 所以在每次调用
select
之前,你都需要重新设置你感兴趣的文件描述符集合 - 常见的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
fd_set readfds; int max_fd; while (1) { // 清除集合 FD_ZERO(&readfds); // 添加你感兴趣的文件描述符 FD_SET(sockfd, &readfds); // 记录最大的文件描述符号,加1 max_fd = sockfd + 1; // 调用select,等待文件描述符变得可读 int activity = select(max_fd, &readfds, NULL, NULL, NULL); // 检查返回值,处理可读的文件描述符 if (activity > 0) { if (FD_ISSET(sockfd, &readfds)) { // sockfd可读,进行处理... } } } |
pollfd
- 要监视的文件描述符
- 指定我们感兴趣的事件,如
POLLIN
(可读),POLLOUT
(可写)等 - 在调用返回时由内核设置,以报告哪些请求的事件实际上是就绪的
1 2 3 4 5 |
struct pollfd { int fd; /* 文件描述符 */ short events; /* 监视的事件 */ short revents; /* 返回的事件 */ }; |
poll
概述
poll
是另一种I/O多路复用机制- 与
select
相比,poll
提供了更直接和灵活的接口来监视多个文件描述符的状态 - 与
select
使用三个不同的描述符集合不同,poll
使用一个pollfd
结构数组,其中每个结构对应一个描述符
- 与
原型
1 2 3 |
int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
参数
fds
是一个指向pollfd
结构数组的指针nfds
是数组中的项数timeout
是等待时间(以毫秒为单位)
返回值
- 正值
- 返回的正数表示已经就绪的文件描述符的数量
换句话说,它告诉你有多少个文件描述符在指定的事件(如可读、可写等)上已经就绪
- 返回的正数表示已经就绪的文件描述符的数量
0
- 表示指定的超时时间已经过去,但没有文件描述符就绪
这是一个正常的情况,表示在给定的时间内没有任何活动
- 表示指定的超时时间已经过去,但没有文件描述符就绪
- 负值
- 如果返回值为负,则表示发生了错误
优点
- 不需要每次调用前重新设置文件描述符集合
- 没有文件描述符数量的固定限制,因为它是基于数组,而不是固定大小的位集合
缺点
- 虽然
API
使用起来相对简单,但与select
相比,在性能方面通常没有显著的改进 - 在大量描述符的情况下,遍历所有描述符可能会相对低效
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#define NUM_FDS 2 struct pollfd fds[NUM_FDS]; int timeout = 1000; // 1秒 fds[0].fd = fd1; // 第一个文件描述符 fds[0].events = POLLIN; // 检测可读 fds[1].fd = fd2; // 第二个文件描述符 fds[1].events = POLLIN; // 检测可读 int ret = poll(fds, NUM_FDS, timeout); if (ret > 0) { // 检查每个文件描述符的状态 for (int i = 0; i < NUM_FDS; i++) { if (fds[i].revents & POLLIN) { // fds[i].fd可读 } } } else if (ret == 0) { // 超时 } else { // 错误 } |
epoll_event
- 感兴趣的事件,如
EPOLLIN
、EPOLLOUT
等 - 用户定义的数据,可以是文件描述符或其它信息
1 2 3 4 |
struct epoll_event { uint32_t events; // Epoll events epoll_data_t data; // User data variable }; |
epoll
概述
epoll
是Linux
操作系统专有的I/O
多路复用机制,它被设计用于解决大量并发连接的高性能监视问题- 相比于
select
和poll
,epoll
更加高效,特别是在大量文件描述符需要被监视时
相关函数
- 此参数可以设置为
0
或EPOLL_CLOEXEC
- 如果设置为
EPOLL_CLOEXEC
,则在执行 exec 调用新程序时会关闭文件描述符
- 如果设置为
- 返回值
- 成功:返回一个文件描述符,代表新创建的
epoll
实例 - 失败:返回
-1
,并设置errno
- 成功:返回一个文件描述符,代表新创建的
1 |
int epoll_create1(int flags); |
- 由
epoll_create1
返回的epoll
实例描述符 - 操作类型,如
EPOLL_CTL_ADD
(添加)、EPOLL_CTL_MOD
(修改)、EPOLL_CTL_DEL
(删除) - 要操作的文件描述符
- 指向
struct epoll_event
的指针,描述感兴趣的事件和其他信息 - 返回值
- 成功:返回
0
- 失败:返回
-1
,并设置errno
- 成功:返回
1 2 3 4 |
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
- 由
epoll_create1
返回的epoll
实例描述符 - 指向
struct epoll_event
数组的指针,用于接收已触发的事件 events
数组的大小- 等待的毫秒数,
-1
表示无限等待,0
表示立即返回 - 返回值
- 成功:返回准备好的文件描述符数量
- 失败:返回
-1
,并设置errno
- 超时:返回
0
1 2 3 4 |
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
详细
- 创建
epoll
实例:- 首先通过调用
epoll_create
或epoll_create1
函数来创建一个epoll
实例
- 首先通过调用
- 注册感兴趣的事件:
- 然后,使用
epoll_ctl
来注册文件描述符和对应的感兴趣的事件(如读、写等) - 这些信息保存在内核中,而非每次调用都重新传递
- 然后,使用
- 等待事件发生:
- 通过调用
epoll_wait
来等待感兴趣的事件发生 - 不同于
select
和poll
每次调用都遍历所有文件描述符,epoll
只返回已经准备好的文件描述符,因此更加高效
- 通过调用
- 处理事件:
epoll_wait
返回后,程序可以处理返回的事件列表,不需要遍历和检查所有描述符
优点
- 能够高效地处理大量并发连接
- 不需要每次都传递所有监视的文件描述符
- 可以更灵活地添加、修改和删除监视的文件描述符和事件
示例
1 2 3 4 5 6 7 8 9 10 11 |
int epfd = epoll_create1(0); // 创建epoll实例 struct epoll_event ev; ev.events = EPOLLIN; // 监视可读事件 ev.data.fd = fd; // 文件描述符 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); // 添加文件描述符到epoll实例 struct epoll_event events[MAX_EVENTS]; int nfds = epoll_wait(epfd, events, MAX_EVENTS, TIMEOUT); // 等待事件 for(int n = 0; n < nfds; ++n) { // 处理事件 } |
epoll理解
文件描述符
epoll_create1
函数的返回值是一个文件描述符(file descriptor
),该描述符代表新创建的epoll
实例- 在
Linux
系统中,许多资源(如文件、套接字、管道等)都可以通过文件描述符来引用和操作,epoll
实例也不例外
注册事件
epoll_ctl
函数用于控制epoll
实例,并允许你在内核中注册、修改或删除文件描述符及其对应的感兴趣的事件- 调用
epoll_ctl
并使用EPOLL_CTL_ADD
操作来注册一个文件描述符时
实际上是在告诉内核我们对该描述符的某些特定事件(如可读、可写等)感兴趣
对应结构体和文件描述符一同存储在内核的数据结构中
- 调用
- 这个机制允许我们有效地监视多个文件描述符的状态,而无需不断轮询它们
- 当事件发生时,我们可以使用
epoll_wait
函数来检索这些事件
- 当事件发生时,我们可以使用
等待事件
- 使用
epoll_ctl
和EPOLL_CTL_ADD
操作注册一个文件描述符和相应的感兴趣事件时,内核不会立即通知任何东西- 需要使用
epoll_wait
函数来等待这些事件发生
- 需要使用
- 调用
epoll_wait
时,它会阻塞,直到注册的文件描述符上发生了感兴趣的事件或超时- 返回时,
epoll_wait
将填充一个epoll_event
数组,其中包括已发生的事件及其对应的文件描述符 - 具体是,内核将检查已注册的文件描述符,并将之前注册的感兴趣的事件匹配的事件填充到数组中
事件填充到数组中的顺序可能是不确定的,但它们都将是在你调用epoll_wait
之前已准备好的事件
- 返回时,
处理事件
- 可以迭代
epoll_event
数组并根据事件类型处理每个事件
清理
- 当完成时,使用
close
关闭epoll
实例
epoll是否可以理解为通知
- 可以将
epoll
的工作方式理解为一种通知机制- 当你使用
epoll_ctl
注册文件描述符和感兴趣的事件后,内核会开始监视这些文件描述符 - 当感兴趣的事件发生时,内核会“通知”应用程序
- 当你使用
- 这个通知不是通过异步信号或其他中断机制来实现的,而是通过将事件信息存储在内部数据结构中
- 然后,当应用程序调用
epoll_wait
时,内核将检查已经发生的事件,并将它们填充到提供的数组中 - 这样,应用程序就可以知道哪些文件描述符已经准备好进行读取或写入
- 然后,当应用程序调用
- 进一步理解:
- 先向内核注册要监听的文件描述符,并指明感兴趣的事件
- 当注册的文件描述符上发生了感兴趣的事件,内核会将事件的信息存储在内部数据结构中
并不是所有的事件会被记录 - 当调用
epoll_wait
时,该调用可能会阻塞(除非设置了超时或使用非阻塞方式)
一旦有一个或多个感兴趣的事件发生,epoll_wait
将返回,并将这些事件填充到提供的数组中
而返回的整数值表示了填充到数组中的事件数量
ET模式
概述
- 边缘触发(
Edge-Triggered, ET
)模式是epoll
提供的一种高级选项,用于通知感兴趣的事件 - 这种模式下,当事件从未发生到发生状态的转变时,
epoll
仅通知应用程序一次
一次性通知
- 在边缘触发模式下,当感兴趣的事件首次发生时,应用程序会被通知
- 只要应用程序不完全处理该事件,例如读取所有可用的数据,即使条件仍然满足(例如仍有数据可读),应用程序将不会再次被通知
非阻塞操作
- 由于边缘触发模式只通知一次事件的发生,因此通常需要将文件描述符设置为非阻塞,并确保在通知后完全处理该事件
- 否则,如果事件没有完全处理,应用程序可能会错过后续的事件通知
性能优势
- 边缘触发模式通常用于高性能网络编程,因为它减少了内核与应用程序之间的通信开销
- 在高流量环境中,减少不必要的通知可以提高效率
复杂性
- 由于边缘触发模式的一次性通知特性,正确使用它可能会更加复杂
- 必须仔细设计代码以确保不会丢失事件,并能够适当地处理部分完成的操作(例如部分读取或写入)
启用方式
- 要启用边缘触发模式,你可以在调用
epoll_ctl
时将EPOLLET
标志与感兴趣的事件组合- 例如,要以边缘触发模式监视读取事件,你可以使用
EPOLLIN | EPOLLET
- 例如,要以边缘触发模式监视读取事件,你可以使用
组合标志
- 除了基本的读取和写入事件,边缘触发模式还可以与其他
epoll
标志和选项结合使用- 例如
EPOLLONESHOT
,该选项表示只通知一次事件,之后将自动将文件描述符从epoll
实例中删除
- 例如
LT模式
概述
- 水平触发(
Level-Triggered
,LT
)是epoll
的默认工作模式 - 这种模式下,当文件描述符准备好进行读取或写入时,
epoll
会一直报告该描述符,直到条件不再满足为止
重复通知
- 在水平触发模式下,只要文件描述符处于可读或可写的状态,每次调用
epoll_wait
都会返回该描述符的事件- 这意味着即使你没有读取或写入文件描述符,只要条件仍然满足,你将继续收到通知
不需要完全消费
- 水平触发模式下,你不需要在接收通知后完全消费文件描述符的所有数据
- 例如,如果一个套接字可读,并且你只读取了一部分数据,下一次调用
epoll_wait
时你仍然会收到可读事件的通知,只要还有更多数据可读
- 例如,如果一个套接字可读,并且你只读取了一部分数据,下一次调用
相似于select和poll
- 水平触发模式的行为与传统的
select
和poll
函数类似- 如果文件描述符的状态没有改变,每次检查都会返回相同的结果
区别于边缘触发
- 主要区别在于通知频率
- 在边缘触发模式下,文件描述符的状态变化会触发通知,但只有在状态从“不可用”变为“可用”时才会通知一次
- 水平触发会在描述符可用时持续通知
iocp
概述
IOCP
(I/O
Completion Ports
)是Windows
操作系统提供的一种高效的I/O
多路复用模型- 与
Unix/Linux
下的epoll
和kqueue
类似,IOCP
提供了一种可扩展的方式来同时处理大量I/O
操作,但它是针对Windows
的异步I/O
设计的
异步I/O操作
IOCP
基于异步I/O
- 当你开始一个
I/O
操作时,你可以提供一个称为“重叠结构”的数据结构(OVERLAPPED
结构),该结构包括用于后续处理的信息 - 如果
I/O
操作不能立即完成,函数调用会立即返回,让调用线程继续执行其他任务
- 当你开始一个
I/O完成端口
I/O
完成端口是一个特殊的句柄,可以与一个或多个文件、套接字或其他I/O
句柄关联- 当与
I/O
完成端口关联的句柄上的异步I/O
操作完成时,完成通知会排队到端口
工作者线程
- 通常会有多个线程等待在同一个
I/O
完成端口上 - 当
I/O
操作完成时,其中一个线程会被唤醒来处理结果- 这些线程通常称为“工作者线程”,它们共享对
I/O
完成端口的访问,从而实现了线程池的效果
- 这些线程通常称为“工作者线程”,它们共享对
I/O完成通知
- 当异步
I/O
操作完成时,与I/O
完成端口关联的工作者线程之一会接收到通知- 通知包括
I/O
操作的结果和先前传递的“重叠结构”,其中包括操作的详细信息
- 通知包括
可扩展性
IOCP
为处理大量并发I/O
操作提供了一种可扩展的方法- 工作者线程可以有效地在多个处理器核心之间分配,而
I/O
完成端口可以管理大量I/O
句柄的通知
- 工作者线程可以有效地在多个处理器核心之间分配,而
使用
- 创建
1 |
CreateIoCompletionPort |
- 关联句柄
- 将文件、套接字或其他
I/O
句柄与I/O
完成端口关联
- 将文件、套接字或其他
- 启动异步
I/O
操作- 使用
Windows
的异步I/O
函数启动操作,并提供重叠结构
- 使用
- 等待和处理
I/O
完成通知- 工作者线程使用
GetQueuedCompletionStatus
函数等待I/O
完成通知,并处理结果
- 工作者线程使用
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Skia总结概述11/15
- ♥ COM组件_403/07
- ♥ 线程和协程10/31
- ♥ Linux_ 命令大全 电子邮件与新闻组03/16
- ♥ Windows 核心编程 _ 内核对象:线程同步一07/29
- ♥ Socket:相关记述11/09