I/O多路复用——epoll

说完了select和poll,那么必须要说一下epoll的。

select和poll是UNIX当中的,epoll是Linux所特有的。

和前面的思路一样,poll解决了select的缺点,所以epoll的出现也是为了最大化的提高多路复用的效率,解决poll的缺点。

epoll介绍


epoll的实现和select与poll的实现有很大的差异,epoll不像select和poll一样通过一个系统调用来完成任务,通过一组函数,epoll把用户关系的文件描述符的时间放在内核的一个事件表中,这样就不用每次都进行向内核传递文件描述符了。epoll使用一个额外的文件描述符来表示内核的事件表,所以这里第一个函数epoll_create就是做这个的。

epoll_create

int epoll_create(int size);

size现在并不起作用,给内核提示,告诉内核事件表有多大。这个函数返回的文件描述符用作其他的epoll相关函数的参数。指向要访问的内核事件表。

然后,我们又有一个函数就是epoll_ctl。

epoll_ctl

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

这个函数用来进行向事件表注册事件,参数epfd就是我们epoll_create函数的返回值,
op是操作类型,有三种。

op参数说明
EPOLL_CTL_ADD向事件表中注册fd上的事件
EPOLL_CTL_MOD修改fd上的注册事件
EPOLL_CTL_DEL删除fd上的注册事件

第三个参数是epoll_event结构指针类型,我们首先来看epoll_event结构。

struct epoll_event {
               uint32_t     events;    /* Epoll events */
               epoll_data_t data;      /* User data variable */
           };

这里的events也是一个即是输入型参数,又是输出型参数,你所关心的监听事件,你jinx设置events,然后如果发生事件,你也可以从events进行获得。
events的参数和poll中的类似,前面加了E。

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

后面两个参数EPOLLET和EPOLLONESHOT是epoll独有的,它们提供给了epoll高效的操作,后续进行介绍。

data是一个结构:


 typedef union epoll_data {
               void    *ptr;
               int      fd;
               uint32_t u32;
               uint64_t u64;
           } epoll_data_t;

这个共用体提供了一些成员来供用户使用。一般来说使用最多的是fd,它指定事件所属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但是这是一个联合,所以我们想要实现一个快速的数据访问,我们可以采取其他办法,比如说我们创建一个结构体,让ptr指向这个结构体,这个结构体当中保存这数据和fd。

最后,我们来说一下epoll_wait

epoll_wait


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

epoll来进行等待文件描述符的事件。这个函数成功的话,范围就绪文件描述符的数量,失败返回-1.

第一个参数epfd,就是epoll_create所得到的返回值,也就是内核事件表。然后第二个参数是结构体数组,这个结构体元素的结构我们在上面讲epoll_ctl的时候已经说过。这个数组用于输出epoll_wait检测到的就绪事件,并不是一个即输入,又输出的参数,这样就可以大大地提高了程序找到就绪文件描述符的效率。maxevents指的是监听事件个数,也就是数组的大小。最后的timeout,设置超时时间,参数与poll的是一样的。

epoll的实际上,当你进行create的时候,这个时候内核进行准备你要监控的文件描述符,然后当是哟个epoll_ctl的使用,其实就是往内核中继续加入新的文件描述符。

epoll的底层实现原理

epoll的高效和epoll的底层远离是分不开的,所以我们需要关注三个关键的问题,一个是mmap,一个是红黑树,一个是链表。。

内核与用户空间通过mmap进行同一块内存的映射,将内核空间的一块地址和用户空间的一块地址映射到相同的一块物理内存。这样就减少了像select和poll那样的从用户态和内核态之间的数据的拷贝。

红黑树将存储epoll所监听的套接字。上面mmap出来的内存放置一棵红黑树,用来存储所有的套接字,当进行add或者del的时候,都从红黑树上去处理,这样时间复杂度就可以保持在O(logn)。

当添加事件以后,这个事件就会和相应的设备驱动程序建立回调关系,当相应的时间发生的时候,这个时候就会去调用回调函数。回调函数就完成了把时间添加到链表当中。

在linux,一切皆文件.所以当调用epoll_create时,内核给这个epoll分配一个file,但是这个不是普通的文件,而是只服务于epoll。

所以当内核初始化epoll时,会开辟一块内核高速cache区,用于安置我们监听的socket,这些socket会以红黑树的形式保存在内核的cache里,以支持快速的查找,插入,删除.同时,建立了一个就绪链表,用于存储准备就绪的事件.所以调用epoll_wait时,在timeout时间内,只是简单的观察这个就绪链表是否有数据,如果没有,则睡眠至超时时间到返回;如果有数据,则拷贝至用户态events数组中.

当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

关于LT模式和ET模式


epoll有两种模式LT(水平触发)和ET(边缘触发),LT模式下,主要缓冲区数据一次没有处理完,那么下次epoll_wait返回时,还会返回这个句柄;而ET模式下,缓冲区数据一次没处理结束,那么下次是不会再通知了,只在第一次返回,所以在ET模式下,一般是通过while循环,一次性读完全部数据,epoll默认使用的是LT。我们可以通过ctl函数进行工作模式的设置。

其实实质就是当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。

经常看到比较ET和LT模式到底哪个效率高的问题。有一个回答是说ET模式下减少epoll系统调用。这话没错,也可以理解,但是在ET模式下,为了避免数据饿死问题,用户态必须用一个循环,将所有的数据一次性处理结束。所以在ET模式下下,虽然epoll系统调用减少了,但是用户态的逻辑复杂了,write/read调用增多了。所以这不好判断,要看用户的性能瓶颈在哪。

总结

epoll的出现,高效解决了服务器高并发,下面说一下它的优点。

  1. 支持最大数目的文件描述符(fd)
     epoll的文件描述符支持最大的打开数目,一般来说1G可以存10万左右。

  2. IO效率不随fd数目增加下降
     这个主要原因是因为epoll不采用轮询,采用活跃的fd进行调用callback回调机制实现的。

  3. mmap实现
     epoll的实现中使用了mmap映射机制,这样可以避免不必要的内存拷贝,用户空间和内核空间在同一块内存实现,避免了多余的copy。

示例代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<sys/epoll.h>
#include<string.h>
#define __SIZE__ 1024
typedef struct epbuf
{
    int fd;
    char buf[1024];
}epbuf,* epbuf_p;

int startup(int port,char *ip)
{
    assert(ip);
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(2);
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);

    if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        perror("bind");
        exit(3);
    }
    if(listen(sock,5) < 0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}

epbuf_p alloc_epbuf(int sfd)
{
    epbuf_p buf = malloc(sizeof(struct epbuf));
    if(buf == NULL)
    {
        perror("malloc");
        exit(5);
    }
    buf->fd = sfd;
    return buf;
}
void del_epbuf(epbuf_p ptr)
{
    if(ptr != NULL)
    free(ptr);
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        printf("Usage : %s [local_ip] [local_port]\n",argv[0]);
        return 1;
    }
    int listen_sock = startup(atoi(argv[2]),argv[1]);

    int epfd = epoll_create(5);

    if(epfd < 0)
    {
        perror("epoll_create");
        return 6;
    }

    struct epoll_event listen_event;
    listen_event.data.ptr = alloc_epbuf(listen_sock);
    listen_event.events = EPOLLIN | EPOLLERR;
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&listen_event) < 0)
    {
        perror("epoll_ctl");
        return 7;
    }

    int timeout = 2000;

    struct epoll_event epfds[__SIZE__];
    while(1)
    {
        int retfd;
        switch((retfd = epoll_wait(epfd, epfds, __SIZE__, timeout)))
        {
            case -1:
            perror("epoll_wait");
            return 8;
            break;
            case 0:
            printf("timeout\n");
            break;
            default:
            {
                int i = 0;
                for(;i < retfd;i++)
                {
                    int fd = ((epbuf_p)(epfds[i].data.ptr))->fd;
                    if((fd == listen_sock) && (epfds[i].events & EPOLLIN))
                    {
                        struct sockaddr_in peer;
                        socklen_t len = sizeof(peer);
                        int ret = accept(fd, (struct sockaddr *)&peer, &len);

                        printf("IP : %s,port : %d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
                        if(ret < 0)
                        {
                            perror("accept");
                            continue;
                        }

                        epfds[i].data.ptr = alloc_epbuf(fd);
                    }//listen_sock
                    else if((fd != listen_sock) && (epfds[i].events & EPOLLIN))
                            {
                                int _r = read(fd,((epbuf_p)(epfds[i].data.ptr))->buf,__SIZE__-1);
                                if(_r < 0)
                                {
                                    perror("read");
                                    return 8;
                                }
                                else if(_r == 0)
                                {
                                    close(fd);
                                    continue;
                                }
                                else
                                {
                                    char *tmp = ((epbuf_p)(epfds[i].data.ptr))->buf;
                                    tmp[_r] = 0;
                                    printf("%s\n",tmp);
                                    epfds[i].events = EPOLLOUT;
                                    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &epfds[i]);
                                }
                            }//sock
                            else if ((fd != listen_sock) && (epfds[i].events & EPOLLOUT))
                    {
                                char *msg="http/1.0 2000ok\r\n\r\n<html><h1>hello world</h1></html>";
                                write(fd, msg, strlen(msg));
                    }
                }
            }//default
        }//switch
    }

    return 0;
}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值