select
介绍
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
功能:将多个文件描述符集中到一起监视
参数:
- nfds:监视对象文件描述符的数量,传入最大的文件描述符值 +1
- readfds:被监视“是否存在待读取数据”的文件描述符集合
- writefds:被监视“是否可传输无阻塞数据”的文件描述符集合
- exceptfds:被监视“是否发生异常”的文件描述符集合
- timeout:超时信息
返回值:
- 发生错误返回 -1,此时 timeout 的值是不确定的,设置 errno 为 EINTR
- 超时返回 0
- 正常返回发生事件的文件描述符数,向其传递的 fd_set 变量将发生变化,除被监视事件发生的文件描述符外,剩下所有位被置 0
简单使用
设置文件描述符,指定监视范围,设置超时,调用 select 函数,查看调用结果
// 程序功能:监视标准输入
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#define BUF_SIZE 1024
char buf[BUF_SIZE];
int main()
{
fd_set reads;
FD_ZERO(&reads);
FD_SET(0, &reads);
while (1) {
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int result;
fd_set tmp = reads;
result = select(1, &tmp, NULL, NULL, &timeout);
if (result == -1) {
puts("select() error!");
break;
} else if (result == 0) {
puts("Time out!");
} else {
if (FD_ISSET(0, &tmp)) {
int len = read(0, buf, BUF_SIZE);
buf[len] = '\0';
printf("message from console: %s", buf);
}
}
}
return 0;
}
注意事项
- 调用 select 函数后,timeval 类型变量的时间将被替换为超时前剩余时间,因此每次调用 select 前都要重新设置
- 将准备好的 fd_set 变量复制到 tmp 中传入,因为除发生变化的文件描述符对应位外,剩下的都被置 0,为了保存初值,必需复制
poll
介绍
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:和 select 类似,在指定时间内轮询一定数量的文件描述符,测试其中是否有就绪者
参数:
-
fds:pollfd 数组,指定所有感兴趣的文件描述符上发生的可读、可写和异常等事件
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
常见事件类型:
POLLIN
POLLERR
-
nfds:被监听事件集合 fds 的大小
-
timeout:以 ms 为单位的等待事件,传递 -1 会一直等待直到事件发生
返回值:
- 发生错误返回 -1
- 超时返回 0
- 正常返回发生事件的文件描述符数
简单使用
// 程序功能:监视标准输入
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#define BUF_SIZE 1024
#define POLL_SIZE 30
char buf[BUF_SIZE];
struct pollfd fds[POLL_SIZE];
int main()
{
fds[0].fd = 0;
fds[0].events |= POLLIN;
while (1) {
int result;
result = poll(fds, POLL_SIZE, -1);
if (result == -1) {
puts("poll() error!");
break;
} else if (result == 0) {
puts("Time out!");
} else {
if (fds[0].recents & POLLIN) {
int len = read(0, buf, BUF_SIZE);
buf[len] = '\0';
printf("message from console: %s", buf);
}
}
}
return 0;
}
注意事项
无
epoll
介绍
#include <sys/epoll.h>
int epoll_create(int size);
功能:创建保存 epoll 文件描述符的空间
参数:
- size:epoll 实例的大小
返回值:
- 成功时返回 epoll 文件描述符
- 失败时返回 -1
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:向空间注册或注销文件描述符
参数:
-
epfd:epoll 例程的文件描述符
-
op:指定对监视对象的操作
- EPOLL_CTL_ADD
- EPOLL_CTL_DEL
- EPOLL_CTL_MOD
-
fd:监视对象文件描述符
-
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 */ epoll_data_t data; /* User data variable */ };
常见事件类型:
EPOLLIN:需要读取数据的情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLRDHUP:TCP 连接被对方关闭,或对方关闭了写操作
EPOLLONESHOT:最多触发该文件描述符上注册的一个可读、可写或异常事件,且只触发一次
返回值:
- 成功返回 0
- 失败返回 -1
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:等待文件描述符发生变化
参数:
- epfd:epoll 例程的文件描述符
- events:保存发生事件的结构体数组地址
- maxevents:可保存的最大事件数
- timeout:以 ms 为单位的等待事件,传递 -1 会一直等待直到事件发生
返回值:
- 成功时返回发生事件的文件描述符数
- 失败返回 -1
简单使用
// 程序功能:监视标准输入
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <sys/epoll.h>
5
6 #define BUF_SIZE 1024
7 #define EPOLL_SIZE 50
8
9 char buf[BUF_SIZE];
10 struct epoll_event ep_events[EPOLL_SIZE];
11
12 int main()
13 {
14 int epfd = epoll_create(EPOLL_SIZE);
15
16 struct epoll_event event;
17 event.events = EPOLLIN;
18 event.data.fd = 0;
19 epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
20
21 while (1) {
22 int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
23 if (event_cnt == -1) {
24 puts("epoll_wait() error!");
25 break;
}
27
28 for (int i = 0; i < event_cnt; i++) {
29 if (ep_events[i].data.fd == 0) {
30 int len = read(0, buf, BUF_SIZE);
31 buf[len] = '\0';
32 printf("message from console: %s", buf);
33 }
34 }
35 }
36
37 close(epfd);
38
39 return 0;
40 }
注意事项
-
epoll_create() 返回的文件描述符主要用于区分 epoll 例程。需要终止时,也要调用 close 函数
-
即使使用 ET 模式,一个 socket 上的某个事件还可能被触发多次。比如一个线程在读取完一个 socket 上的数据后开始处理,而在处理过程中该 socket 上又有新数据可读,此时另一个线程被唤醒来读取新数据,于是出现了两个线程同时操作一个 socket 的局面
- 为了解决该问题,要为连接 socket 注册 EPOLLONESHOT,当然一旦事件处理完毕,该线程要立即重置该 socket 上的 EPOLLONESHOT,以确保该 socket 下一次可读时,其 EPOLOLIN 事件能触发,让其他工作线程有机会处理
水平触发与边缘触发
水平触发:只要输入缓冲有数据就会一直通知该事件,epoll 默认以水平触发模式工作
边缘触发:输入缓冲收到数据时仅注册一次该事件。即使输入缓冲中还有数据也不会再进行注册
实现边缘触发:
- 通过 error 变量验证错误原因
- 为了完成非阻塞 IO,更改套接字特性