Linux的I/O复用技术:epoll

epoll:

epoll是Linux特有的IO复用函数,被认为性能最好的一种方法,
它和select、poll在实现和使用上有很大差异:
1.使用一组函数来完成,而不是单个
2.把用户关心的文件描述符上的事件放在内核的一个事件表中,无须向select、poll那样每次调用都要重复传入文件描述符集或事件集,
但epoll需要用一个额外的文件描述符来表示内核中的这个事件表

epoll函数非常简单,有epoll_create,epoll_ctl,epoll_wait3个函数,先使epoll_create创建一个epoll的句柄,再通过epoll_ctl注册事件,然后epoll_wait检测事件的发生
优点:
具备了select所不具备的所有优点
1.没有fd数量的限制,它所支持的fd上限是最大可以打开文件的数目,具体数目可cat/proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大;
2.epoll_ctl每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝,
epoll保证了每个fd在整个过程中只会拷贝一次;
3.epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)
并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。
epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd;
适用场景:
当活动连接比较多的时候,epoll_wait的效率未必比select和poll高,因为此时回调函数被触发的过于频繁,因此epoll_wait适用于连接数量多,但活动连接较少的情况。

epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文

操作epoll的内核事件表

件描述符使用如下epoll_create函数来创建:

#include<sys/epoll.h>
int epoll_create(int size);
/*
  size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。
  该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
*/
操作epoll的内核事件表:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
  fd参数是要操作的文件描述符,op参数则指定操作类型。操作类型有如下3种:
  (1) EPOLL_CTL_ADD,往事件表中注册fd上的事件。
  (2) EPOLL_CTL_MOD,修改fd上的注册事件.
  (3) EPOLL_CTL_DEL,删除fd上的注册事件。
  event参数指定事件,它是 epoll_event结构指针类型。 epoll_event的定义如下:
  struct epoll_event
  {
    __uint32_t events;//epo11事件 
    epoll_data_t data; //用户数据
  };
  其中 events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是 EPOLLIN。
但epoll有两个额外的事件类型————EPOLLET和 EPOLLONESHOT。 data成员用于存储用户数据,其类型 epoll_data_t的定义如下: typedef union epoll_data { void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; fd指定事件所从属的目标文件描述符; ptr成员可用来指定与fd相关的用户数据; 由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员。
*/
epoll_wait函数在一段超时时间内等待一组文件描述符上的事件,其原型如下:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents ,int timeout);
/*
  该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。
  
  maxevents参数指定最多监听多少个事件,它必须大于0。
  epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数 events指向的数组中。
  这个数组只用于输出 epoll_wait检测到的就绪事件,而不像 select和poll的数组参数那样既用于传入用户注册的事件,
  又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。

  epoll对文件描述符的操作有两种模式:LT(电平触发)模式和ET(边沿触发)模式。LT模式是默认的工作模式,
  这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的 EPOLLET事件时,
  epoll将以ET模式来操作该文件描述符。ET模式是epoll高效工作模式。
  ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

  LT模式:只要socket上有未读完的数据,就会一直产生事件;
  ET模式:socket上每新来一次数据就会触发一次,如果上一次触发后,未将socket上的数据读完,也不会再触发,除非新来一次数据;
*/

epollserver.cpp:

#include<sys/types.h>
#include<sys/msg.h>
#include<sys/ipc.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<sys/wait.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/ipc.h>
#include<errno.h>
#include<sys/shm.h>
#include<fcntl.h>
#include<semaphore.h>
#include<arpa/inet.h>
#include<iostream>
#include<assert.h>
#include<ctype.h>
#include<time.h>
using namespace std;
#define MS(a,b) memset(a,b,sizeof(a))

//将文件描述符设置成非阻塞的
int setnonblocking(int fd)
{
    int oldt = fcntl(fd, F_GETFL);
    int newt = oldt | O_NONBLOCK;
    fcntl(fd, F_SETFL, newt);
    return oldt;
}
/*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数flag指定是否对fd启用ET模式*/
void addfd(int epfd, int fd, bool flag)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if (flag)
    {
        event.events |= EPOLLET;
    }
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}
//LT模式的工作流程
int LT(epoll_event* events, int num, int epfd, int listenfd)
{
    char buf[10];
    int i,sockfd;
    printf("LT num: %d\n", num);
    for (i=0; i<num; i++)
    {
        sockfd = events[i].data.fd;
        printf("LT sockfd: %d, listenfd: %d\n", sockfd, listenfd);
        if (sockfd == listenfd)
        {
            struct sockaddr_in cAddr;
            socklen_t cAddrLen = sizeof(cAddr);
            int connfd = accept(listenfd, (struct sockaddr*)&cAddr, &cAddrLen);
            addfd(epfd, connfd, false); //对connfd禁用ET模式
        }
        else if (events[i].events & EPOLLIN)
        {
            /*只要socket上读缓存中还有未读出的数据,这段代码就被触发*/
            printf("LT event trigger once\n");
            MS(buf,'\0');
            int ret = recv(sockfd, buf, 10-1, 0);
            if (ret < 0)
            {
                close(sockfd);
                continue;
            }
            printf("LT get %d bytes of content: %s\n", ret, buf);
        }
        else 
        {
            printf("LT something error!");
        }
    }
    
}
//ET模式的工作流程
int ET(epoll_event* events, int num, int epfd, int listenfd)
{
    char buf[10];
    int i,sockfd;
    printf("ET num: %d\n", num);
    for (i=0; i<num; i++)
    {
        sockfd = events[i].data.fd;
        printf("ET sockfd: %d, listenfd: %d\n", sockfd, listenfd);
        if (sockfd == listenfd) //有新的连接
        {
            struct sockaddr_in cAddr;
            socklen_t cAddrlen = sizeof(cAddr);
            int connfd = accept(listenfd, (struct sockaddr*)&cAddr, &cAddrlen);
            addfd(epfd, connfd, true);
        }
        else if (events[i].events & EPOLLIN) //接收到数据,读socket
        {
            /*这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出*/
            printf("ET event trigger once\n");
            while(1)
            {
                MS(buf,'\0');
                int ret =recv(sockfd, buf, 10-1, 0);
                if (ret < 0)
                {
                    /*对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。此后,epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作*/
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        printf("ET read later\n");
                        break;
                    }
                    close(sockfd);
                    break;
                }
                else if (ret == 0)
                {
                    close(sockfd);
                }
                else 
                {
                    printf("ET get %d bytes of content: %s\n", ret, buf);
                }
            }
        }
        else
        {
            printf("ET something error!");
        }
    }
}
int main(int argc, char* argv[])
{
    if (argc <= 2)
    {
      printf("argc error!\n");
      return -1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int ret = 0;
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &addr.sin_addr);
    addr.sin_port = htons(port);
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);
    
    ret = bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != -1);
    
    epoll_event events[1024];
    int epfd = epoll_create(5);
    assert(epfd != -1);
    addfd(epfd, listenfd, true);
    while(1)
    {
        int ret = epoll_wait(epfd, events, 1024, -1);
        if (ret < 0)
        {
            printf("epoll fail!\n");
            break;
        }
       // LT(events, ret, epfd, listenfd); /*使用LT模式*/
        ET(events, ret, epfd, listenfd); /*使用ET模式*/
    }
    close(listenfd);
    return 0;
}

epollclient.cpp:

#include<sys/types.h>
#include<sys/msg.h>
#include<sys/ipc.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<sys/wait.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/ipc.h>
#include<errno.h>
#include<sys/shm.h>
#include<fcntl.h>
#include<semaphore.h>
#include<arpa/inet.h>
#include<iostream>
#include<assert.h>
#include<ctype.h>
#include<time.h>
using namespace std;
#define MS(a,b) memset(a,b,sizeof(a))

int main(int argc, char* argv[])
{
    if (argc <= 2)
    {
      printf("argc error!\n");
      return -1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int ret = 0;
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &addr.sin_addr);
    addr.sin_port = htons(port);
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);
    printf("epoll client fd: %d\n", listenfd);
    ret = connect(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    assert(ret != -1);
    
     
    while(1)
    {
        char buf[128] = {0x00};
        int i = 0,ret;
        sprintf(buf, "hanyufengloveliuyifei: %d",++i);
        ret = send(listenfd, buf, strlen(buf), 0);
        printf("ret: %d\n", ret);
        assert(ret >= 0);
        MS(buf,0);
        ssize_t len = recv(listenfd, buf, sizeof(buf)-1, 0);
        assert(len >= 0);
        printf("epoll client recv data: [%s]\n", buf);
        sleep(2);
    }

    return 0;
}

 

 
posted @ 2023-06-03 23:01  韓さん  阅读(31)  评论(0编辑  收藏  举报