您的位置:首页 > 理论基础 > 计算机网络

UNIX网络编程4 使用epoll

2015-08-18 10:33 381 查看
使用:http://blog.chinaunix.net/uid-20384806-id-1954307.html

优点:http://blog.csdn.net/wangpengqi/article/details/9933011

源码分析 http://blog.csdn.net/chen19870707/article/details/42525887
int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核需要建通的数目一共有多大,epoll句柄会占用一个fd值。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,第一个参数是创建的epoll句柄,第二个参数表示动作:

EPOLL_CTL_ADD 注册新的fd到epfd中

EPOLL_CTL_MOD 修改已经注册的fd的监听事件

EPOLL_CTL_DEL 从epfd中删除一个fd

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事

struct epoll_event {

_unit32_t events;

epoll_data_t data;

}

events可以是一下几个宏的组合

EPOLLOIN 表示对应的文件描述符可以读,包括对端的socket正常关闭

EPOLLOUT 可写

EPOLLPRI 有紧急的数据可读

EPOLLERR 发生错误

EPOLLHUP 被挂断

EPOLLET 将EPOLL设为边缘触发,这是相对于水平触发来说的

EPOLLONESHOT 只监听一次事件,完了还还需要监听这个fd的话需要重新加入到EPOLL队列里

epoll_data_t data是一个结构体,里面包含了fd。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数events用来从内核得到事件的集合,maxevents告知内核这个events有多大,timeout可以设超时毫秒或立即返回或阻塞,

该函数返回要处理的事件数目,或者超时无事件0。

水平触发:LT,默认工作方式,同时支持block和non-block socket,内核通知就绪以后如果不对就绪的fd进行IO操作,内核还是会继续通知你。

边缘触发:ET,高速工作方式,只支持non-block socket,当描述符从未就绪变为就绪时,内核会通过epoll告诉你,以后不会再为那个文件描述符发送更多的就绪通知,

直到下次有新的数据进来的时候才会再次触发就绪事件。

使用epoll的一个服务端程序(不完善):

//:epollServer.c
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

#define SERV_PORT 6000
#define MAX_FD 1024
#define BUF_SIZE 1024
#define EVENTS_NUM 20
#define TIMEOUT 1000
#define MAX_BACK 20

void setnonblocking(int sock)
{
int opts;
opts = fcntl(sock, F_GETFL);
if (opts < 0)
{
perror("fcntl(sock, GETFL)");
exit(1);
}
opts = opts | O_NONBLOCK;
if (fcntl(sock, F_SETFL, opts) < 0)
{
perror("fcntl(sock, SETFL, opts)");
exit(1);
}
}
int main(int argc, char** argv)
{
int listenfd, connfd, sockfd, epfd, nfds, n = 0, i = 0;
char line[BUF_SIZE+1];
socklen_t clilen;
struct epoll_event ev, events[EVENTS_NUM];
epfd = epoll_create(MAX_FD);
struct sockaddr_in serveraddr, clientaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd);

ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET;
//注册epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERV_PORT);
bzero(line, BUF_SIZE);
bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, MAX_BACK);

for (; ;)
{
//等待epoll事件的发生
nfds = epoll_wait(epfd, events, EVENTS_NUM, TIMEOUT);
for (i = 0; i < nfds; i ++)
{
// 在listenfd上发生读事件,说明有新的客户端连接
if (events[i].data.fd == listenfd)
{
//sockaddr 和sockaddr_in的区别:二者占用的内存空间一样大可互相转化,前者在socket.h中后者在in.h中,
//sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
//而sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
connfd = accept(listenfd, (struct sockaddr * )&clientaddr, &clilen);
if (connfd < 0)
{
perror("accept error, connfd < 0");
}
setnonblocking(connfd);
char *str = inet_ntoa(clientaddr.sin_addr);
printf("Accept a connection from %s\n", str);
//注册这个connfd的ev
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

}
//已连接的客户,收到数据
else if (events[i].events & EPOLLIN)
{
printf("EPOLLIN\n");
if ((sockfd = events[i].data.fd) < 0)
continue;
//注意这里,边缘触发应该保证读完缓冲区
//可以用循环来反复读取,当n==0的时候表示读完
if ((n = read(sockfd, line, BUF_SIZE)) < 0)
{
if (errno == ECONNRESET)
{
close(sockfd);
events[i].data.fd = -1;
}
else
{
printf("Readline error\n");
}
}
else if (n == 0)
{
printf("Null message\n");
close(sockfd);
events[i].data.fd = -1;
}
else
{
line
= '\0';
printf("Read from %d: %s\n", sockfd, line);

//注册写操作事件
//ev.data.fd = sockfd;
//ev.events = EPOLLOUT | EPOLLET;
//epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
//这里直接写回给客户端,没有再使用epoll调度写事件
write(sockfd, line, n);
}
}
//如果有数据要发送,本程序中没什么用。。。
else if (events[i].events & EPOLLOUT)
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
//有错的话是否需要close
else
{
printf("Other events!\n");
//close(events[i].data.fd);
events[i].data.fd = -1;
}
}
}

//epoll fd 会占用一个文件句柄
close(epfd);
return 0;
}


epoll的优势(参考http://blog.csdn.net/chen19870707/article/details/42525887):

①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:

在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。

1.执行epoll_create时,创建了红黑树和就绪list链表。

2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。

3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: