关于epoll的详细介绍和编程例子(文章来自KENGINE | Kankanews.com)

12 篇文章 0 订阅
0 篇文章 0 订阅

两种不同的触发器模型:边沿触发(ET:Edge-Trigger)和水平触发(LT:Level-Trigger)

    1. 边沿触发(ET:Edge-Trigger):当调用前条件是unready,并且调用过程中条件变成ready,才给予调用者通知。

    假设调用代码是ret = check(condition);,如果调用时的condition是ready的,那么即使接下来condition还是ready,check函数返回给ret还是无效通知或者不返回(阻塞式)。当且仅当condition的值是unready时,然后在调用的过程中,condition变成了ready,那么此时check函数会返回一个有效通知给ret。

    2. 水平触发(LT:Level-Trigger):当调用时条件是ready,就给予调用者通知。

    假设调用代码是ret = check(condition);,只要调用时,condition是ready的,check函数就会返回一个有效通知给ret,否则返回无效通知或者不返回(阻塞式)。

    首先,什么叫把condition的条件变成ready或者unready。假设有个这样的场景:我们监听一个网络套接字sockfd上是否有数据可读,假定有数据可读就是ready状态,没数据可读就是unready状态。

    容易出错的往往是ET模型。假设sockfd上一开始是没有数据可读(unready),然后我调用ret = check(sockfd);,以阻塞式为例吧,那么此时线程会一直阻塞在check里面。某一时刻,远方有人往该sockfd上发来数据,那么该sockfd的状态就由unready变成了ready,显而易见,check应该返回一个有效通知,此时该线程收到通知,然后去读。注意看,接下来有两种情况:1)线程开始读取sockfd的数据,直到全部读取完毕,然后又调用check(sockfd);2)线程开始读取sockfd的数据,没有全部读取完毕,然后又调用check(sockfd)。首先说结果,1)是正确的,2)是错误的。然后说为什么。相信已经能够理解,把ready转换成unready的操作就是全部读完sockfd的数据,把unready转换成ready的操作就是远方有人发数据到该sockfd来了。2)错就错在,没有把sockfd的数据读干净,以至于没有把状态变成unready,那么即使后面又来了新数据,check(sockfd);还是会一直阻塞下去。所以使用ET模型的时候,一定要注意,每次收到有效通知,然后读取数据的时候,务必每次读干净(读到出错为止)。当再次调用check(sockfd);的时候才能正确返回。

    那么LT模型却不会出现ET模型中的2)情况。为什么呢?设想一下,即使你没有读取完数据,又去调用check(sockfd);,因为LT的特性,只要是ready状态就发有效通知,所以,你懂的!

epoll()是什么,它诞生于什么时候,它在哪诞生,它诞生的意义是什么?

    epoll()类似于poll()和select(),也是一种能是的I/O多路复用的机制,它诞生于Linux内核2.6版本,它在Linux可用,它是一种它综合后面二者的优势,剔除后面二者的弊端,它诞生的意义完全就是来取代后面二者的。

epoll()比起select()和poll()来说,好在哪?

    1. select():一次性可以同时监控FD_SETSIZE个fds,FD_SETSIZE在编译阶段才被确定的,但是很明显,FD_SETSIZE是固定的、有限的。

    2. poll():一次性可监控的fds个数是不固定的,看起来这是个好处,比select()灵活且更多,但是问题来了,每一次调用,我们需要对传进去的所有的fds进行一次线性扫描,将花掉O(n)的时间,这个消耗很大。

    3. epoll():顾名思义——Enhanced POLL,poll()的加强版。epoll()对于一次性监控的文件描述符个数没有限制(poll()的长处),不需要做线性扫描(避免poll()的短处)。可想而知,epoll()的性能比二者更高。

为什么epoll()有两种工作模式,LT和ET,哪种更高端、大气?

    ET比LT更高端、更装逼。当然装逼有风险,也意味着ET更难用,用不好更容易出错。

epoll()怎么用?

    epoll机制包含以下几个操作方法和数据结构:

    操作方法:

    1. int epoll_create(int size);

    返回值是一个epoll描述符,size是你想创建一个多大的监听队伍!

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

    往epfd队列里注册一个op方式的fd,要求监控fd的event系列的事件

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

    等待epfd队列中的所有注册的fd发生事件,一般timeout用-1,表示永久阻塞,timeout单位是毫秒。

    PS:本人最近在学习iOS开发,事实上,当一个函数的参数上升到4、5个之后,要记住每个参数的意义就显得麻烦了,除非你天天用。那么Objective-C的优势就体现出来了,它会标记出每个参数的意义,相当注释了。

   数据结构:

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正常关闭);

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

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

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

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

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

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

从代码出发吧!!!已在Fedora-19-64bit通过编译,运行测试OK

 #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 <string.h>
#include <stdlib.h>
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000
#define LOCAL_HOST "192.168.1.100" // 改成你电脑的IP地址
void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL); // 把sock的属性查询出来,存到opts中
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts | O_NONBLOCK; // 把opts中的某一位O_NONBLOCK置为生效
    if(fcntl(sock,F_SETFL,opts)<0) // 再设回去,搞定
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}
int main()

{
    int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
    struct epoll_event ev,events[20]; //声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件
    epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式
    ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
    ev.events=EPOLLIN | EPOLLET; //设置要处理的事件类型和使用的触发器的机制
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //把listenfd注册到epfd队列中,监听EPOLLIN事件,使用EPOLLET机制的触发器
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr = LOCAL_HOST;
    inet_aton(local_addr,&(serveraddr.sin_addr));
    serveraddr.sin_port=htons(SERV_PORT);
    bind(listenfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for( ; ; )
    {
        static int count = 0;
        nfds=epoll_wait(epfd,events,20,-1); //等待epoll事件的发生,第四个参数设置为-1,永久阻塞
        printf("[%d] nfds = [%d]\n", ++count, nfds); // 现实当前处于第几次epoll_wait内
        for(i=0; i<nfds; ++i) //处理所发生的所有事件
        {
            if(events[i].data.fd==listenfd)    /**当产生事件的fd和开始监听的fd相同,那么肯定是新客户端请求来了**/
            {
                connfd = accept(listenfd,(struct sockaddr *)&clientaddr, &clilen);
                if(connfd<0)
                {
                    perror("connfd<0");
                    exit(1);
                }
                setnonblocking(connfd); //把客户端的socket设置为非阻塞方式
                char *str = inet_ntoa(clientaddr.sin_addr);
                printf("connect from %s \n", str);
                ev.data.fd=connfd; //设置用于读操作的文件描述符
                ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev事件
            }
            else if(events[i].events&EPOLLIN)     /**当这个fd上有读事件发生时**/
            {
                if ( (sockfd = events[i].data.fd) < 0)
                    continue; //因为下面设置了,所以这里考虑了
                if ( (n = read(sockfd, line, MAXLINE)) < 0) // 如果read返回值小于零,表示系统调用被中断,要分开考虑错误情况
                {
                    if (errno == ECONNRESET) // 表示连接被重置了,已经无效了,关闭它,删除它
                    {
                        printf("ECONNRESET: %s\n", strerror(ECONNRESET));
                        epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); // 可以把这个sockfd从epfd队列中删除了
                        close(sockfd);
                    }
                    else // 其他错误
                    {
                        printf("readline error\n");
                    }
                }
                else if (n == 0) // 有读事件触发,但是read返回0,所以是对面已经关闭socketfd了
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); // 可以把这个sockfd从epfd队列中删除了
                    close(sockfd);
                }
                else
                {
                    ev.data.fd=sockfd;
                    ev.events=EPOLLOUT | EPOLLET;
                    epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); // 下次监听写事件
                }
            }
            else if(events[i].events&EPOLLOUT)    /**当这个fd上有写事件发生时**/

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                ev.data.fd=sockfd;
                ev.events=EPOLLIN | EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLIN,下次监听读事件
            }
        }
    }
} 


好的,接下来我会详细介绍 epollepollLinux 内核提供的一种 I/O 事件通知机制,与 select 和 poll 相比,它具有更高的性能和更强的扩展性。 epoll 的特点如下: 1. 支持边缘触发和水平触发两种模式。 2. 采用基于事件驱动的方式,可以同时监听大量的文件描述符。 3. 支持添加、修改和删除事件,可以动态地改变监听的文件描述符。 4. 可以通过 epoll_wait 函数等待事件的发生,避免了轮询的效率问题。 epoll 的使用流程如下: 1. 调用 epoll_create 函数创建一个 epoll 实例。 2. 调用 epoll_ctl 函数向 epoll 实例添加、修改或删除事件。 3. 调用 epoll_wait 函数等待事件的发生。 4. 处理已经发生的事件。 下面是一个简单的使用 epoll 的示例: ```c #include <sys/epoll.h> int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); struct epoll_event event, events[10]; int epfd, nfds; epfd = epoll_create(1); event.data.fd = fd; event.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); while(1) { nfds = epoll_wait(epfd, events, 10, -1); for(int i = 0; i < nfds; i++) { if(events[i].data.fd == fd) { // 处理文件描述符 fd 的事件 } } } return 0; } ``` 在这个示例中,我们创建了一个 epoll 实例,并向其中添加了一个文件描述符,然后使用 epoll_wait 函数等待事件的发生,并处理已经发生的事件。 需要注意的是,epoll 的事件驱动模型并不是线程安全的,因此在多线程环境下需要进行同步处理。 希望这份介绍对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值