首页 > 代码库 > epoll
epoll
Epoll 是一种高效的管理socket的模型,相对于select和poll来说具有更高的效率和易用性。传统的select以及poll的效率会因为 socket数量的线形递增而导致呈二次乃至三次方的下降,而epoll的性能不会随socket数量增加而下降。标准的linux-2.4.20内核不支持epoll,需要打patch。本文主要从linux-2.4.32和linux-2.6.10两个内核版本,并介绍epoll在游戏研发中服务器后台的 玩家结构 应用。
1、很多基于socket的应用只设置一个监听端口,而在实际的游戏后台开发中,需要设置一个监听端口组数,即同时监听多个端口,分担一个端口处理accept的压力及风险;处理这个端口数组的游戏前台对应的connect;
2、玩家前端connect server的一个监听端口listen_fd后,server建立玩家player与accept生成的fd的对应关系;建立此plyer与fd映射,有多种方法,主要是灵活应用struct epoll_event中的epoll_data_t结构,在后面的例子中会说明;.
player[i].tcp_fd = connfd;
ev.events = EPOLLIN| EPOLLET;
ev.data.ptr = &(player[i]);
epoll_ctrl(kdpfd, EPOLL_CTL_ADD, connfd, &ev);
3、数据到来时候,通过epoll_data_t结构的fd 或ptr 或u32 反查至player, 从而把解析后的游戏包,添加到player缓存上去;
recvplayer = eventsplayer[i].data.ptr;
readsize = getfromsocket(recvplay->tcp_fd, buffer);
4、fd异常时候,需要清理玩家或者设置玩家的处理状态,从而分步卸载玩家;
二、 Epoll的使用epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
所用到的数据结构
typedef union epoll_data {
void * ptr; //可以设置为 玩家指针 ; 而玩家身上标有自己的fd
int fd; //可以设置为 玩家自己的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 /
};
结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值为:EPOLLIN :表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符设定为edge模式;
所用到的函数:
1、epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。
2、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值
EPOLL_CTL_ADD 注册、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1
3、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回。
返回发生事件数。-1有错误。
举一个简单的例子:
C/C++ codeint main()
{
//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
struct epoll_event ev,events[20];
epfd=epoll_create(10000); //创建epoll句柄
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//把socket设置为非阻塞方式
setnonblocking(listenfd);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = INADDR_ANY;
serveraddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr )&serveraddr, sizeof(serveraddr));
listen(listenfd, 255);
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events=EPOLLIN;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
for ( ; ; )
{
//等待epoll事件的发生
nfds=epoll_wait(epfd,events,20,1000);
//处理所发生的所有事件
for(i=0;i<nfds;++i)
{
if(events .data.fd==listenfd)
{
connfd = accept(listenfd,(struct sockaddr )&clientaddr, &clilen);
if(connfd<0)
{
perror("connfd<0");
}
setnonblocking(connfd);
//设置用于读操作的文件描述符
ev.data.fd=connfd;
//设置用于注测的读操作事件
ev.events=EPOLLIN|EPOLLET;
//注册event
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
else if(events .events&EPOLLIN)
{
read_socket(events .data.fd);
ev.data.fd=events .data.fd;
ev.events=EPOLLIN|EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else if(events .events&EPOLLOUT)
{
write_socket(events .data.fd);
ev.data.fd=events .data.fd;
ev.events=EPOLLIN|EPOLLET; //ET模式
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else
{
perror("other event");
}
}
}
}
Epoll的ET模式与LT模式
ET(Edge Triggered)与LT(Level Triggered)的主要区别可以从下面的例子看出
eg:
1. 标示管道读者的文件句柄注册到epoll中;
2. 管道写者向管道中写入2KB的数据;
3. 调用epoll_wait可以获得管道读者为已就绪的文件句柄;
4. 管道读者读取1KB的数据
5. 一次epoll_wait调用完成
如果是ET模式,管道中剩余的1KB被挂起,再次调用epoll_wait,得不到管道读者的文件句柄,除非有新的数据写入管道。如果是LT模式,只要管道中有数据可读,每次调用epoll_wait都会触发。
另一点区别就是设为ET模式的文件句柄必须是非阻塞的。
三、 Epoll的实现
Epoll 的源文件在/usr/src/linux/fs/eventpoll.c,在module_init时注册一个文件系统 eventpoll_fs_type,对该文件系统提供两种操作poll和release,所以epoll_create返回的文件句柄可以被poll、 select或者被其它epoll epoll_wait。对epoll的操作主要通过三个系统调用实现:
1. sys_epoll_create
2. sys_epoll_ctl
3. sys_epoll_wait
下面结合源码讲述这三个系统调用。
1.1 long sys_epoll_create (int size)
该系统调用主要分配文件句柄、inode以及file结构。在linux-2.4.32内核中,使用hash保存所有注册到该epoll的文件句柄,在该系统调用中根据size大小分配hash的大小。具体为不小于size,但小于2size的2的某次方。最小为2的9次方(512),最大为2的17次方(128 x 1024)。在linux-2.6.10内核中,使用红黑树保存所有注册到该epoll的文件句柄,size参数未使用。
1.2 long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
1. 注册句柄 op = EPOLL_CTL_ADD
注册过程主要包括:
A.将fd插入到hash(或rbtree)中,如果原来已经存在返回-EEXIST,
B.给fd注册一个回调函数,该函数会在fd有事件时调用,在该函数中将fd加入到epoll的就绪队列中。
C.检查fd当前是否已经有期望的事件产生。如果有,将其加入到epoll的就绪队列中,唤醒epoll_wait。
2. 修改事件 op = EPOLL_CTL_MOD
修改事件只是将新的事件替换旧的事件,然后检查fd是否有期望的事件。如果有,将其加入到epoll的就绪队列中,唤醒epoll_wait。
3. 删除句柄 op = EPOLL_CTL_DEL
将fd从hash(rbtree)中清除。
1.3 long sys_epoll_wait(int epfd, struct epoll_event events, int maxevents,int timeout)
如果epoll的就绪队列为空,并且timeout非0,挂起当前进程,引起CPU调度。
如果epoll的就绪队列不空,遍历就绪队列。对队列中的每一个节点,获取该文件已触发的事件,判断其中是否有我们期待的事件,如果有,将其对应的epoll_event结构copy到用户events。
revents = epi->file->f_op->poll(epi->file, NULL);
epi->revents = revents & epi->event.events;
if (epi->revents) {
……
copy_to_user;
……
}
需要注意的是,在LT模式下,把符合条件的事件copy到用户空间后,还会把对应的文件重新挂接到就绪队列。所以在LT模式下,如果一次epoll_wait某个socket没有read/write完所有数据,下次epoll_wait还会返回该socket句柄。
四、 使用epoll的注意事项
1. ET模式比LT模式高效,但比较难控制。
2. 如果某个句柄期待的事件不变,不需要EPOLL_CTL_MOD,但每次读写后将该句柄modify一次有助于提高稳定性,特别在ET模式。
3. socket关闭后最好将该句柄从epoll中delete(EPOLL_CTL_DEL),虽然epoll自身有处理,但会使epoll的hash的节点数增多,影响搜索hash的速度。
Q:网络服务器的瓶颈在哪?
A:IO效率。
在大家苦苦的为在线人数的增长而导致的系统资源吃紧上的问题正在发愁的时候,Linux 2.6内核中提供的System Epoll为我们提供了一套完美的解决方案。传统的select以及poll的效率会因为在线人数的线形递增而导致呈二次乃至三次方的下降,这些直接导致了网络服务器可以支持的人数有了个比较明显的限制。
自从Linux提供了/dev/epoll的设备以及后来2.6内核中对/dev /epoll设备的访问的封装(System Epoll)之后,这种现象得到了大大的缓解,如果说几个月前,大家还对epoll不熟悉,那么现在来说的话,epoll的应用已经得到了大范围的普及。
那么究竟如何来使用epoll呢?其实非常简单。
通过在包含一个头文件#include 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait范围之后应该是一个循环,遍利所有的事件:
C/C++ codefor(n = 0; n < nfds; ++n) {
if(events[n].data.fd == listener) { //如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。
client = accept(listener, (struct sockaddr ) &local,
&addrlen);
if(client < 0){
perror("accept");
continue;
}
setnonblocking(client); // 将新连接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。
注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,如果有写操作的话,这个时候epoll是不会返回事件的,如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
// 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,这里用EPOLL_CTL_ADD来加一个新的 epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
fprintf(stderr, "epoll set insertion error: fd=d0,
client);
return -1;
}
}
else // 如果不是主socket的事件的话,则代表是一个用户socket的事件,则来处理这个用户socket的事情,比如说read(fd,xxx)之类的,或者一些其他的处理。
do_use_fd(events[n].data.fd);
}