]I/O多路转接(二)之epoll服务器

什么是epoll?

按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是 2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通 知方法。

epoll的相关系统调用 epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。

(1). int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。

需要注意的是,当创建好 epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这 个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

(2). int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而 是在这里先注册要监听的事件类型。

第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd。 第四个参数是告诉内核需要监听什么事,struct 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可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket的话,需要再次把这个socket加入到EPOLL队列里

(3). int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout); 收集在epoll监控的事件中已经发送的事件。

参数events是分配好的epoll_event结构体数组, epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复 制到这个events数组中,不会去帮助我们在用户态中分配内存)。
maxevents告之内核这个 events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时 时间(毫秒,0会立即返回,-1是永久阻塞)。如果函数调用成功, 返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

epoll工作原理

epoll同样只告知那些就绪的⽂描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一 个数组中依次取得相应数量的⽂文件描述符即可,这里也使用了内存映射(mmap)技术,这 样便彻底省掉了这些⽂文件描述符在系统调用时复制的开销。 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select中,进程只有在调 用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机 制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

Epoll的2种工作方式-水平触发(LT)和边缘触发(ET)

epoll的优点:

1.支持一个进程打开大数目的socket描述符(FD)

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认 值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是 可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来⽹网络效率的下降, 二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比 较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不 是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的 数目,这个数字一般远大于2048.

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之上了。

* 3.使用mmap加速内核与用户空间的消息传递*

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通 知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户 空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记 手工 mmap这一步的。

server端代码:

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

#define EP_SIZE 64

static void usage(const char* proc)  //使用说明
{ 
     printf("Usage:%s [local_ip] [local_port]\n", proc);
}

int startup(const char* _ip, int _port) //创建套接字
{ 

    int sock = socket(AF_INET, SOCK_STREAM , 0);
    if(sock < 0)
    { 
        perror("socket\n");
        exit(2);
    }

    struct sockaddr_in local;

    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = inet_addr(_ip);

    if(bind(sock, (struct sockaddr*)&local, sizeof(local))<0)
    {
        perror("bind\n");
        exit(3);
    }

    if(listen(sock, 10) < 0)
    { 
        perror("listen\n");
        exit(4);
    }
    return sock;
}

//tcp_server 127.0.0.1 8080
int main(int argc, char* argv[])
{ 
    if(argc != 3)
    { 
        usage(argv[0]);
        return 1;
    }
    int listen_sock = startup(argv[1], atoi(argv[2]));
    int epfd = epoll_create(EP_SIZE); //创建epoll模型
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    //添加事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
    int num = -1; //就绪事件数目
    struct epoll_event revs[EP_SIZE];  //就绪队列
    int timeout = 5000; // 定时器
    while(1)
    { 
        switch(num = epoll_wait(epfd, revs, EP_SIZE, timeout))
        { 
            case 0:
                printf("timeout\n");
                break;
            case -1:
                perror("wait");
                break;
            default:
                { 
                    int i = 0;
                    for(; i<num; i++)
                    { 
                        int sock = revs[i].data.fd;
                        if(sock == listen_sock && revs[i].events == EPOLLIN)
                            // 监听事件就绪
                        { 
                            struct sockaddr_in client;
                            ssize_t len = sizeof(client);
                            int new_sock = accept(sock, (struct sockaddr*)&client, &len);
                            if(new_sock < 0)
                            { 
                                perror("accept");
                                continue;
                            }
                             printf("get a new client:%s:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                            ev.events = EPOLLIN;
                            ev.data.fd = new_sock;
                            //加入到epoll模型中
                            epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
                        }
                        else if(sock != listen_sock)
                        { 
                            char buf[1024];
                            if(revs[i].events & EPOLLIN)//普通读事件
                            {           
                                ssize_t s = read(sock, buf, sizeof(buf)-1);
                                if(s > 0)
                                { 
                                    buf[s] = 0;
                                    printf("client# : %s\n", buf);
                                    ev.events = EPOLLOUT;
                                    ev.data.fd = sock;
                                    epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
                                    continue;
                                }
                                else if(s == 0)
                                {
                                    printf("client is quit...\n");
                                    close(sock);
                                    epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                    continue;
                                }
                                else
                                { 
                                    perror("read");
                                    close(sock);
                                    epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                    continue;
                                }
                            }//fi
                            if(revs[i].events & EPOLLOUT) //普通写事件
                            {
                                char buf[1024];  
                                printf("Please Enter# ");
                                fflush(stdout);                              
                                ssize_t s = read(0, buf, sizeof(buf)-1);
                                if(s > 0)
                                { 
                                    buf[s-1] = 0;
                                    s = write(sock, buf, strlen(buf));
                                    if(s > 0)
                                    { 
                                        buf[s] = 0;
                                        printf(" server# : %s\n", buf);
                                        ev.events = EPOLLIN;
                                        ev.data.fd = sock;
                                        epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
                                        break;
                                    }
                                    else if(s == 0)
                                    { 
                                        close(sock);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                        continue;
                                    }
                                    else
                                    { 
                                        perror("write");
                                        close(sock);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                        continue;
                                    }
                                    //continue;
                                }
                                else if(s == 0)
                                { 
                                    printf("client is quit...\n");
                                    close(sock);
                                    epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                    continue;
                                }
                                else
                                { 
                                    perror("read");
                                    close(sock);
                                    epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
                                    continue;
                                }
                            }           
                        }//else fi
                    }//for
                }//default
        }//switch
    }//while
    return 0;   
}

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

static void usage(const char* proc)  //使用说明
{ 
     printf("Usage:%s [local_ip] [local_port]\n", proc);
}


//tcp_client server_ip server_port
int main(int argc, char* argv[])
{ 
    if(argc != 3)
    { 
        usage(argv[0]);
        return 1;
    }

    int sock = socket(AF_INET, SOCK_STREAM , 0);
    if(sock < 0)
    { 
        perror("socket\n");
        return 2;
    }

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    { 
        perror("connect\n");
        return 3;
    }

    printf("connect success\n");
    char buf[1024];
    while(1)
    { 
        printf("please Enter# \n");
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof(buf)-1);
        if(s > 0)
        { 
            buf[s-1] = 0;
            write(sock, buf, strlen(buf));
            s = read(sock, buf, sizeof(buf)-1);
            if(s > 0)
            { 
                buf[s] = 0;
                printf("server echo# %s\n ", buf);
            }
        }
    }
    close(sock);
    return 0;
}

运行结果:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值