Chinaunix首页 | 论坛 | 博客
  • 博客访问: 627729
  • 博文数量: 227
  • 博客积分: 8017
  • 博客等级: 中将
  • 技术积分: 2069
  • 用 户 组: 普通用户
  • 注册时间: 2007-12-08 22:50
文章分类

全部博文(227)

文章存档

2011年(10)

2010年(55)

2009年(28)

2008年(134)

我的朋友

分类: 系统运维

2010-10-30 17:26:16

epoll是Linux内核为处理大批量fd而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

一. epoll优点:
1. 支持一个进程打开大数目的socket描述符
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的应用服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2. IO效率不随fd数目增加而线性下降。
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃” 的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket 进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会。在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。


二. epoll使用:
epoll相关的系统调用有:epoll_create, epoll_ctl和epoll_wait。Linux-2.6.19又引入了可以屏蔽指定信号的epoll_pwait。其中:
  1)epoll_create用来创建一个epoll文件描述符

  2)epoll_ctl用来添加/修改 /删除需要侦听的文件描述符及其事件
  3)epoll_wait/epoll_pwait接收发生在被侦听的描述符上的,用户感兴趣的IO事件。
epoll文件描述符用完后,直接用close关闭即可,非常方便。事实上,任何被侦听的文件符只要其被关闭,那么它也会自动从被侦听的文件描述符集合中删除,很是智能。
  
每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用 epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。

1. int epoll_create(int size);
创建一个epoll的fd,参数size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数(maxfd+1)。在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create返回的epoll fd,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epoll实例中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epoll中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

typedef union epoll_data {
  void        *ptr;
  int          fd;
  __uint32_t   u32;
  __uint64_t   u64;
} epoll_data_t;

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

网络IO中的边缘触发和水平触发
边缘触发是指每当状态变化时发生一个IO事件,条件触发是只要满足条件就发生一个IO事件。举个读socket的例子,假定经过长时间的等待后,现在来了100个字节,这时无论边缘触发和条件触发都会产生一个sockfd readable event通知应用程序可读。假设应用程序读了50个字节,然后重新调用API等待IO事件。这时条件触发的API会因为还有50个字节可读从而立即返回用户一个sockfd readable event。而边缘触发的API会因为可读这个状态没有发生变化而陷入长期等待。因此在使用边缘触发的API时,要注意每次都要读到socket返回EWOULDBLOCK为止,否则这个socket就算废了。而使用条件触发的API时,如果应用程序不需要写就不要关注socket可写的事件,否则就会无限次的立即返回一个sockfd writable event。

epoll的两种工作模式
1) LT(level triggered)是缺省的工作方式。内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
2) ET (edge-triggered)是高速工作方式。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。


3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(0表示即使没有事件也立即返回,-1表示永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。



三. 简单使用epoll

1. epoll_server.cpp

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <strings.h>

void err_quit(char *msg)
{
    char buf[1024];
    snprintf(buf, sizeof(buf), "%s", msg);
    perror(buf);
    exit(1);
}
void set_nonblocking(int sfd)
{
    int opts;
    if ((opts=fcntl(sfd,F_GETFL)) < 0)
        err_quit("fcntl get socket flag error");
    opts = opts|O_NONBLOCK;
    if(fcntl(sfd,F_SETFL,opts)<0)
        err_quit("fcntl set socket flag error");
}

#define SERV_PORT 12345
#define SERV_LENSENQ 100

int main(int argc, char *argv[])
{
    int fd, listenfd, connfd;
    int epfd, nfds;
    int i, n;

    char wbuf[] = "abcdefghijk";
    char rbuf[1024] = {0};

    struct sockaddr_in srvaddr, cliaddr;
    socklen_t clilen = sizeof(cliaddr);
    struct epoll_event ev;
    struct epoll_event events[50];

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_quit("socket error");

    //set_nonblocking(listenfd);

    srvaddr.sin_family = AF_INET;
    const char *server_addr = "127.0.0.1";
    inet_aton(server_addr, &srvaddr.sin_addr);
    srvaddr.sin_port = htons(SERV_PORT);
    if (bind(listenfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)) < 0)
        err_quit("bind error");
    if (listen(listenfd, SERV_LENSENQ) < 0)
        err_quit("listen error");

    printf("server listen on %s:%d\n", server_addr, SERV_PORT);
    
    if ((epfd = epoll_create(256)) < 0)
        err_quit("epoll_create error");
    
    ev.data.fd = listenfd;
    ev.events = EPOLLIN;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0)
        err_quit("epoll_ctl for listenfd error");
    
    for(;;){
        nfds = epoll_wait(epfd, events, sizeof(events), 1000);
        for (i = 0; i < nfds; i++){
            if (events[i].data.fd == listenfd){
                printf("listenfd event\n");
                connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
                if (connfd < 0)
                    err_quit("accept error");
                char *str = inet_ntoa(cliaddr.sin_addr);
                printf("accept connect from %s\n", str);
                ev.data.fd = connfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }else if(events[i].events & EPOLLIN){
                printf("connfd read event\n");
                if ((fd = events[i].data.fd) < 0)
                    continue;
                if ((n = read(fd, rbuf, sizeof(rbuf))) < 0){
                    close(fd);
                    events[i].data.fd = -1;
                    printf("read error\n");
                } else if (n == 0) {
                    close(fd);
                    events[i].data.fd = -1;
                }
                rbuf[n] = '\0';
                printf("server read %d bytes: %s\n", n, rbuf);
                
                ev.data.fd = fd;
                ev.events = EPOLLOUT;
                epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
            }else if(events[i].events & EPOLLOUT){
                fd = events[i].data.fd;
                if ((n = write(fd, wbuf, sizeof(wbuf))) < 0){
                    close(fd);
                    events[i].data.fd = -1;
                    printf("write error");
                }
                printf("server write data: '%s' to client\n", wbuf);
                ev.data.fd = fd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
            }
        }
    }//end for(;;)

        
    close(epfd);
    return 0;
}



2. epoll_client.cpp

#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
void err_quit(char *msg)
{
    char buf[1024];
    snprintf(buf, sizeof(buf), "%s", msg);
    perror(buf);
    exit(1);
}

#define SERV_PORT 12345

int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in srvaddr;
    char wbuf[] = "123456789";
    char rbuf[128] = {0};
    int n;
    
    const char *server_addr = "127.0.0.1";
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_port = htons(SERV_PORT);
    inet_aton(server_addr, &srvaddr.sin_addr);
    
    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_quit("socket error");
    if ((n = connect(fd, (struct sockaddr*)&srvaddr, sizeof(srvaddr))) < 0)
        err_quit("connect error");
    printf("connet to server %s:%d\n", server_addr, SERV_PORT);
    
    if ((n = write(fd, wbuf, sizeof(wbuf))) != sizeof(wbuf))
        err_quit("write data to server error");

    printf("write %d bytes to server\n", sizeof(wbuf));

    if ((n = read(fd, rbuf, sizeof(rbuf))) < 0)
        err_quit("read data from server error");
    else
        close(fd);
    
    rbuf[strlen(rbuf)] = '\0';
    printf("read %d bytes data from server, data: %s\n", n, rbuf);

    printf("client go to sleep...\n");
    sleep(60*60);
    close(fd);
    return 0;
}



3.程序的执行:


./epoll_server
server listen on 127.0.0.1:12345
listenfd event
accept connect from 127.0.0.1
connfd read event
server read 10 bytes: 123456789
server write data: 'abcdefghijk' to client
connfd read event
server read 0 bytes:

./epoll_client
connet to server 127.0.0.1:12345
write 10 bytes to server
read 12 bytes data from server, data: abcdefghijk
client go to sleep...


4. 关于EPOLLET模式的使用实例

当上面的实例程序所有的ev.events设置都加上 |EPOLLET并且server只读数据,client只写数据时,会出现边沿触发的特性。详细的可以参考这篇文章:
epoll学习笔记





阅读(891) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~