IO多路复用介绍
- IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
- 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
- 没有文件句柄就绪就会阻塞应用程序,交出CPU。
- IO多路复用的三种实现方式 select/poll/epoll
epoll
epoll的全称是eventpoll,它是基于event事件进行实现的,是linux特有的I/O复用函数。
- 有点
文件描述符没有限制(使用链表存储) - 三个核心函数
epoll_create(建立一个epoll对象)
epoll_ctl(epoll的事件注册函数)
epoll_wait(等待事件的产生,类似于select()调用)
int epoll_create(int size)
在内核中创建epoll实例并返回一个epoll文件描述符。 在最初的实现中,调用者通过 size 参数告知内核需要监听的文件描述符数量。如果监听的文件描述符数量超过 size, 则内核会自动扩容。而现在 size 已经没有这种语义了,但是调用者调用时 size 依然必须大于 0,以保证后向兼容性。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
向 epfd 对应的内核epoll 实例添加、修改或删除对 fd 上事件 event 的监听。
- epfd:epoll_create的返回值
- op:表示动作
EPOLL_CTL_ADD:添加新的事件到文件描述符。
EPOLL_CTL_MOD: 修改文件描述符上的事件类型
EPOLL_CTL_DEL: 删除一个事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件
当 timeout 为 0 时,epoll_wait 永远会立即返回。而 timeout 为 -1 时,epoll_wait 会一直阻塞直到任一已注册的事件变为就绪。当 timeout 为一正整数时,epoll 会阻塞直到计时 timeout 毫秒终了或已注册的事件变为就绪。因为内核调度延迟,阻塞的时间可能会略微超过 timeout 毫秒。
- demo
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<ctype.h>
#define MAXLEN 1024
#define SERV_PORT 8000
#define MAX_OPEN_FD 1024
int main(int argc,char *argv[])
{
int listenfd,connfd,efd,ret;
char buf[MAXLEN];
struct sockaddr_in cliaddr,servaddr;
socklen_t clilen = sizeof(cliaddr);
struct epoll_event tep,ep[MAX_OPEN_FD];
listenfd = socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,20);
// 创建一个epoll fd
efd = epoll_create(MAX_OPEN_FD);
tep.events = EPOLLIN;tep.data.fd = listenfd;
// 把监听socket 先添加到efd中
ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
// 循环等待
for (;;)
{
// 返回已就绪的epoll_event,-1表示阻塞,没有就绪的epoll_event,将一直等待
size_t nready = epoll_wait(efd,ep,MAX_OPEN_FD,-1);
for (int i = 0; i < nready; ++i)
{
// 如果是新的连接,需要把新的socket添加到efd中
if (ep[i].data.fd == listenfd )
{
connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
tep.events = EPOLLIN;
tep.data.fd = connfd;
ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
}
// 否则,读取数据
else
{
connfd = ep[i].data.fd;
int bytes = read(connfd,buf,MAXLEN);
// 客户端关闭连接
if (bytes == 0){
ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL);
close(connfd);
printf("client[%d] closed\n", i);
}
else
{
for (int j = 0; j < bytes; ++j)
{
buf[j] = toupper(buf[j]);
}
// 向客户端发送数据
write(connfd,buf,bytes);
}
}
}
}
return 0;
}
epoll_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 */
} __attribute__ ((__packed__));
原理
可以看到
- epoll_create 创建了一个对象,这个对象包含一个event_poll线程池(红黑树)和rdlist就绪对列表(双向链表)
- epoll_ctl 执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据
- epoll_wait 执行epoll_wait()时立刻返回准备就绪链表里的数据即可。
当事件就绪后,就被加入到 rdlist(就绪链表)中。epoll_wait 检查是否有事件发生时,仅仅需要检查 rdlist 中是否有数据即可。
ep_poll_callback
ep_poll_callback函数核心功能是将被目标fd的就绪事件到来时,将fd对应的epitem实例添加到就绪队列。当应用调用epoll_wait()时,内核会将就绪队列中的事件报告给应用
参考
https://www.modb.pro/db/250807
https://www.cnblogs.com/tangxin-blog/p/5470791.html
https://sites.uclouvain.be/SystInfo/usr/include/sys/epoll.h.html
https://zhuanlan.zhihu.com/p/389407114