当前位置:首页 > 嵌入式培训 > 嵌入式学习 > 讲师博文 > 基于epoll的Linux并发服务器

基于epoll的Linux并发服务器 时间:2018-09-26      来源:未知

基于网络的不同主机之间通信、我们经常使用socket(套接字)来实现,socket就是用于通信的endpoint(端点)。当我们基于socket发送或接受数据时,由于各种原因可能无法将数据发送出去,或者无法接收到数据。这时我们是选择等待、还是立即返回。如果一直等待,我们称之为阻塞I/O;立即返回,则称之为非阻塞I/O。对于Linux系统,socket默认是阻塞的。一个典型的阻塞I/O如下图:

当进程调用read系统调用时,由于没有数据可读,内核需要准备数据。当准备好数据、内核又需要将数据从内核空间拷贝到户用空间。在此期间,应用程序一直在read函数上阻塞等待,直到数据到达。当有多个I/O同时读写时,阻塞I/O模式经常因为当前I/O不可读写而影响其他I/O的数据传输,使效率大大降低。如果使用非阻塞I/O就可以避免这种情况出现。但是如果采用非阻塞模式实现多I/O通信、就必须轮询尝试读写每一个I/O,而一些I/O并不可读写,白白浪费了时间,效率也受到影响。如图:

当用户进程调用read函数时,如果内核中的数据还没有准备好,那么进程并不会阻塞,而是立刻返回错误,错误码可能是EWOULDBLOCK,表示数据没有准备好,于是可以选择重新read。一旦内核中的数据准备好,并且用户此时发起read操作,那么它马上就将数据拷贝到了用户空间内存,然后返回。

不停的检测I/O是否可读写降低了系统效率,做了大量的无用功。幸运的是,有这样一种机制,可以监视多个I/O,一旦某个I/O就绪(可读或可写),就能够通知程序进行相应的读写操作,这种机制称为I/O多路复用。实现这种机制Linux提供了select,poll和epoll函数。

select函数将I/O事件划分成可读,可写和异常,而有时即使通知用户可读写,但实际却没有数据,相当于误报。另外,select监视的多路I/O文件描述符集合、同时也用来保存返回结果,也就是保存哪些描述符可读、可写或发生异常。这就导致我们总要不停的构造监视的I/O文件描述符集合。所以select监视多路I/O的效率不高,也不能监视太多的I/O,一般不应该超过FD_SETSIZE(默认为1024)个I/O。当返回结果后,还要检查哪些描述符在返回的可读,可写或发生异常集中。

poll函数针对select函数的缺陷做了一些改进,将监视的文件描述符与监视的返回结果(哪些可读写)分开,所以poll不需要重新构造监视的I/O文件描述符集合。并且使用位图表示更多的事件。但是poll对select的改进是有限的,都需要向内核拷贝需要监视的I/O文件描述符集合,也需要查看全部描述符,检查哪些描述符已经可读,可写或发生异常等。虽然poll没有描述符个数限制,但文件描述符太多会导致效率下降。

当客户端连接大量增加时,select和poll的效率下降比较多。而且很多时候我们并不仅仅关心哪些描述符可读写,很可能是和该描述符关联的其他任务要处理,而我们只通过文件描述符很难找到关联的其他任务。epoll函数解决了select/poll函数的上述缺陷。它不需要向内核拷贝监视的文件描述符集合,当某些文件描述符就绪时、也不需要轮询检查哪些描述符可读写。当文件描述符可读写时,内核直接返回可读写的文件描述符,以及其相关联的对象指针。这在实际程序中非常有意义。但epoll是Linux内核2.5.44引入,只在Linux系统上才可以使用。

epoll提供三个系统调用,分别如下:

1.int epoll_create(int size)

创建一个epoll的句柄。size用来告诉内核需要监听的数目一共有多大,自从linux2.6.8之后,size参数是被忽略的,内核动态觉得size大小。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在使用完epoll后,必须调用close()关闭。

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

epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

epfd参数是文件描述符,用来引用关联的epoll实例。op表示要完成的操作,分别为:

EPOLL_CTL_ADD,添加新的文件描述符到epoll实例;

EPOLL_CTL_MOD,修改已经注册的文件描述符的监听事件;

EPOLL_CTL_DEL,删除一个文件描述符;

fd就是要操作的文件描述符,event参数告诉内核需要监听什么事,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 */

};

Events是一个32位整数,每一位表示一种事件。用一下宏表示:

EPOLLIN ,表示对应的文件描述符可读(包括对端SOCKET正常关闭);

EPOLLOUT,表示对应的文件描述符可写;

EPOLLPRI,表示对应的文件描述符有紧急的数据可读;

EPOLLERR,表示对应的文件描述符发生错误;

EPOLLHUP,表示对应的文件描述符被挂断;

EPOLLET,将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT,只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到epoll实例中。

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

等待epfd指向的epoll实例上发生关心的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中。maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间,单位是毫秒,如果该值为0,即使没有事件发生也会立即返回,-1表示一直阻塞到有事件发生。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

epoll的man手册上有一个比较好的例子可以参考,代码如下:

#define MAX_EVENTS 10

structepoll_eventev, events[MAX_EVENTS];

intlisten_sock, conn_sock, nfds, epollfd;

/* Set up listening socket, 'listen_sock' (socket(),

* bind(), listen()) */

/* 创建epoll实例 */

epollfd = epoll_create(10);

if (epollfd == -1) {

perror("epoll_create");

exit(EXIT_FAILURE);

}

/* 将socket创建的监听描述符listen_sock添加到epoll实例,以便监听EPOLLIN事件 */

ev.events = EPOLLIN;

ev.data.fd = listen_sock;

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {

perror("epoll_ctl: listen_sock");

exit(EXIT_FAILURE);

}

for (;;) {

/* 等待epollfd指向的epoll实例有关心的事件发生,并将其对应的epoll_event结构存入events指向的内存 */

nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);

if (nfds == -1) {

perror("epoll_pwait");

exit(EXIT_FAILURE);

}

for (n = 0; n

/* 如果有客户端发起连接、就accept建立连接 */

if (events[n].data.fd == listen_sock) {

conn_sock = accept(listen_sock, (structsockaddr *) &local, &addrlen);

if (conn_sock == -1) {

perror("accept");

exit(EXIT_FAILURE);

}

/* 将新连接设置为非阻塞模式 */

setnonblocking(conn_sock);

/* 将新连接添加到epoll实例 */

ev.events = EPOLLIN | EPOLLET;

ev.data.fd = conn_sock;

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {

perror("epoll_ctl: conn_sock");

exit(EXIT_FAILURE);

}

} else {

/* 如果不是有客户端发起连接,而是有文件描述符发生关心的事件,这里就可以处理该事件 */

do_use_fd(events[n].data.fd);

}

}

}

上一篇:JNI开发简介及实例演示

下一篇:什么是"文件表项"

热点文章推荐
华清学员就业榜单
高薪学员经验分享
热点新闻推荐
前台专线:010-82525158 企业培训洽谈专线:010-82525379 院校合作洽谈专线:010-82525379 Copyright © 2004-2022 北京华清远见科技集团有限公司 版权所有 ,京ICP备16055225号-5京公海网安备11010802025203号

回到顶部