接着上一篇,IO多路复用的另外两种实现:poll和epoll。select之前整理过了,这里主要讲poll和epoll的特点,以及epoll的三个核心函数。
一、poll
poll的特点:
1. 使用链表保存文件描述符集合,允许监测的文件描述符没有上限限制
2. 创建的文件描述符集合在应用层,需要应用层和内核层的反复数据拷贝
3. 应用层对于返回的事件结果需要遍历查找
4. poll只能工作在水平触发模式(低速模式),无法工作在边沿触发模式(高速模式)
简单说就是poll解决了select有fd数量上限的问题,但本质上和select差不多,还是要在用户态和内核态之间来回拷贝,返回后还要自己遍历找哪个fd就绪了,而且只有水平触发。
二、epoll
epoll的特点:
1. 使用树形结构(红黑树)保存文件描述符集合,没有文件描述符上限限制,并且查找效率高
2. 文件描述符集合创建在内核层,避免了应用层和内核层的反复数据拷贝
3. 直接返回到达事件的文件描述符集合
4. epoll可以工作在水平触发模式(低速模式),也可以工作在边沿触发模式(高速模式)
epoll比poll和select都高效,核心原因就是集合放在内核里,不用每次调用都拷贝一遍,而且直接返回就绪的fd,不用自己遍历全部。
三、epoll三个核心函数
1. epoll_create — 创建文件描述符集合
int epoll_create(int size);
功能:创建epoll的文件描述符集合。
参数:
– size:允许监测的文件描述符的个数
返回值:
– 成功:返回文件描述符句柄
– 失败:-1
2. epoll_ctl — 添加/修改/删除文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:对文件描述符集合中的文件描述符做操作。
参数:
– epfd:epoll创建的文件描述符集合
– op:操作方法
– EPOLL_CTL_ADD:添加操作
– EPOLL_CTL_MOD:修改操作
– EPOLL_CTL_DEL:删除操作
– fd:要操作的文件描述符
– event:epoll事件结构体
事件结构体定义:
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 */
};
events事件类型:
– EPOLLIN:读事件
– EPOLLOUT:写事件
返回值:
– 成功:0
– 失败:-1
3. epoll_wait — 通知内核开始监测事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能:通知内核开始监测事件。
参数:
– epfd:监测的文件描述符句柄
– events:保存epoll返回的到达事件的集合
– maxevents:最多监测的事件个数
– timeout:设置超时时间,-1表示不设置超时,一直阻塞等待
返回值:
– 成功:实际到达的事件的个数
– 失败:-1
– 0:超时时间到达,却没有事件返回时
四、epoll TCP服务器示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#define MAX_EVENTS 20
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.119");
if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1) {
perror("bind error");
close(sockfd);
return -1;
}
if (listen(sockfd, 5) == -1) {
perror("listen error");
close(sockfd);
return -1;
}
// 1. 创建epoll集合
int epfd = epoll_create(MAX_EVENTS);
if (-1 == epfd) {
perror("epoll_create error");
close(sockfd);
return -1;
}
// 2. 把监听套接字加入epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct epoll_event events[MAX_EVENTS];
char buff[1024] = {0};
while (1) {
// 3. 等待事件
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (-1 == nready) {
perror("epoll_wait error");
break;
}
for (int i = 0; i < nready; i++) {
int fd = events[i].data.fd;
if (fd == sockfd) {
// 有新连接
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &addrlen);
if (-1 == connfd) {
perror("accept error");
continue;
}
printf("客户端 [%s:%d] 接入\\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 把新客户端加入epoll
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else {
// 客户端发数据
memset(buff, 0, sizeof(buff));
ssize_t size = recv(fd, buff, sizeof(buff) – 1, 0);
if (size <= 0) {
printf("客户端断开\\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
continue;
}
printf("收到: %s\\n", buff);
send(fd, buff, size, 0);
}
}
}
close(epfd);
close(sockfd);
return 0;
}
整体流程就是epoll_create创建集合,epoll_ctl把fd加进去,epoll_wait等事件,返回后直接遍历就绪的fd处理就行,比select简洁高效很多。





