epoll中文

44 篇文章 0 订阅
17 篇文章 1 订阅

1          介绍

名字 epoll:具有I/O事件提醒功能

 

头文件 #include <sys/epoll.h>

 

描述 这个epoll API 具有和poll相似的功能。监视多个文件描述符的集合,检测里面是否有I/O事件发生。

epoll API 被分为了两类:水平触发(LT,level-triggered)和边缘触发(ET,edge-triggered),并且能够很好的监视大量文件描述符。

下面几个系统调用提供了创建和控制epoll实例的方法。

epoll_create(2) 创建一个epoll实例并且返回一个引用了epoll实例的文件描述符。最近又多了一个 epoll_create1(2) ,扩展了epoll_create(2)的功能。

通过 epoll_ctl(2),能够插入一个文件描述符,使其注册。这些注册到了一个epoll实例的文件描述符有时被称为一个epoll的集合。

使用epoll_wait(2) 能够等待I/O事件,如果没有任何事件发生的话,其会阻塞调用线程。

2          ET特点

水平触发(LT,level-triggered)和边缘触发(ET,edge-triggered) epoll事件分发有两种模式。

这两种模式的主要区别描述如下,假设下面的场景。

1)一个管道的读端的文件描述符(rfd)注册到了epoll实例。

2)管道的另一边,写端,写入了2kb的数据。

3)调用了epoll_wait(2),并且返回了rfd作为了已经准备好了的事件。

4)管道的读端只是从管道里面读了1kb。

5)又调用了epoll_wait(2)。

如果rfd加入epoll的实例,其使用了EPOLLET 作为flag,那么其为边缘触发模式,在上面第五步调用epoll_wait(2)时,那么将会被挂起。尽管,在管道内仍然有可读数据。同时,管道的另一端可能正在期待一个回应,因为它已经发送了数据。造成这样的原因主要是边缘模式只是传递epoll集合的事件的改变。所以,在此模式下,调用线程也许会一直在等待一些数据被传送到输入缓冲区。在上面这个例子中,rfd的事件之所以会产生是因为在第二步的时候写事件已经完成,这个事件已经激活了第三步。又因为第四步的读操作并没有完全的消费掉整个缓冲区的数据。所以对 epoll_wait(2) 的调用将会在第五步无限期的阻塞下去。

3          ET使用方法

如果应用非要使用EPOLLET 作为flag的epoll的话,务必使用非阻塞的方式,以避免上述情况发生。请使用下述建议:

1)使用非阻塞的文件描述符。

2)只有在read和write返回 EAGAIN 的时候才会去阻塞等待一个事件。

 

相比之下,当使用水平模式(这是默认的模式,如果不使用EPOLLET 作为flag的话,就是这个模式),epoll只是一个更快的poll,并且能够在任何poll使用的地方使用,因为它们两者具有相同的语义。

4          EPOLLONESHOT

即使在ET触发模式的epoll下,对于一个文件描述符而言,由于多次写了了大量的数据,会多次产生事件。那么调用线程也可以使用EPOLLONESHOT 的flag,告诉epoll对于指定的文件描述符,当使用epoll_wait(2)收到一次事件后,就不再理会后序的事件。当EPOLLONESHOT的flag被设定之后,那么调用线程必须注意使用带EPOLL_CTL_MOD epoll_ctl(2) 恢复这个文件描述符到原来的状态

 

5          /proc 接口

下面的接口能够用于间接的限制epoll占用的内存的总量

/proc/sys/fs/epoll/max_user_watches(since Linux 2.6.28)

上面这句主要是用于设定一个最大值,这个最大值是所有的epoll被注册的文件描述符的总量。这个限制是针对每一个真实的USER ID的。

在32位的机器上,每一个被注册的文件描述符都将消耗大约90个字节。在64位的机器上则会大约消耗160个字节。目前,默认的max_user_watches 的值大约可用的低内存的1/25(4%) ,再除以相应机器上每个文件描述符消耗的字节数(64位:160或者32位:90)

 

6          例子

尽管epoll的水平模式下的使用方式、功能与poll(2)完全相同,但是在边缘模式下的使用方式则需要更多的说明以避免一些在事件处理中发生的乱78遭的事。在下面的这个例子中,listener是一个被listen(2)调用所创建的非阻塞的socket。函数do_use_fd() 消费着新的准备好的文件描述符,直到read(2) 和 write(2) 返回 EAGAIN。那么此时,一个以事件为驱动的应用应该在接受到EAGAIN之后,记录其当前的状态。以确保在下一次调用 do_use_fd()函数时,它能够从read(2)或者 write(2) 停止的地方接着开始。(译者:不知道是不是翻译的功力不够,没有看出上面的解释和代码之间的关系。感觉代码在干一件事,但是解说在说另一回事。)

#define MAX_EVENTS10

           struct epoll_event ev, events[MAX_EVENTS];

           int listen_sock, conn_sock, nfds,epollfd;

 

           /* Set up listening socket,'listen_sock' (socket(), bind(), listen()) */

 

           epollfd = epoll_create(10);

           if (epollfd == -1) {

              perror("epoll_create");

               exit(EXIT_FAILURE);

           }

 

           ev.events = EPOLLIN;

           ev.data.fd = listen_sock;

           if (epoll_ctl(epollfd,EPOLL_CTL_ADD, listen_sock, &ev) == -1) {

               perror("epoll_ctl:listen_sock");

               exit(EXIT_FAILURE);

           }

           ev.events = EPOLLIN;

           ev.data.fd = listen_sock;

           if (epoll_ctl(epollfd,EPOLL_CTL_ADD, listen_sock, &ev) == -1) {

               perror("epoll_ctl:listen_sock");

               exit(EXIT_FAILURE);

           }

           for (;;) {

               nfds = epoll_wait(epollfd,events, MAX_EVENTS, -1);

               if (nfds == -1) {

                  perror("epoll_pwait");

                   exit(EXIT_FAILURE);

               }

 

               for (n = 0; n < nfds; ++n) {

                   if (events[n].data.fd ==listen_sock) {

                       conn_sock =accept(listen_sock,

                                       (structsockaddr *) &local, &addrlen);

                       if (conn_sock == -1) {

                          perror("accept");

                           exit(EXIT_FAILURE);

                       }

                      setnonblocking(conn_sock);

                       ev.events = EPOLLIN _EPOLLET;

                       ev.data.fd = conn_sock;

                       if (epoll_ctl(epollfd,EPOLL_CTL_ADD, conn_sock,

                                   &ev) ==-1) {

                           perror("epoll_ctl:conn_sock");

                           exit(EXIT_FAILURE);

                       }

                   } else {

                      do_use_fd(events[n].data.fd);

                   }

               }

           }

 

当使用边缘模式的时候,考虑到性能的需要,是可以通过epoll_ctl函数的EPOLL_CTL_ADD的参数来添加文件描述符。同时要注意可以指明:ev.events = EPOLLIN |EPOLLET; 。这使得我们可以指定我们同时关心这个文件描述符的读事件和写事件。

 

 

7          问题与答案

Q0:有什么方法可以用于区别哪些文件描述符已经注册到epoll集合里面了?

A0:关键就是结合 文件描述符 和 公开文件描述(the open file description ),或者又是被称为:公开文件句柄(open file handle),这是内核对一个文件的内在描述

 

Q1:如果你注册一个相同的文件描述符给同一个epoll实例会发生什么情况?

A1:你也许会的EEXIST。然而,如果是通过复制的方式得到的文件描述符( (dup(2), dup2(2), fcntl(2) F_DUPFD) )那么,你很有可能能够将其注册到同一个epoll实例。对于过滤事件而言,这是一种非常有用的技术,这样可以使得复制得到文件描述符可以注册到不同的事件中。

 

Q2:两个不同epoll能够等待得到同一个文件描述符的事件么?如果可以的,那么事件是不是会同时报告给两个epoll实例呢?

A2:是的。事件会同时报告给两个epoll实例。然而,这样编程的话,需要更加小心。

 

Q3:这个epoll文件描述符自身能否被poll/epoll使用?

A3:可以,如果这个epoll这个文件描述符有事件等待被处理,那么其会报告为可读事件。

 

Q4:如果把epoll这个文件描述符放进其自身的epoll的文件描述符集会发生什么事?

A4:那么调用epoll_ctl(2)将会得到失败((EINVAL))。然而,可以添加一个epoll文件描述符给另一个epoll文件描述符集。

 

Q5:我可以通过unix域套接字发送epoll的文件描述符给另一个进程吗?

A5:可以。但是这样做似乎有点不合理,因为接受这个epoll文件描述符的进程没有注册到这个epoll实例的文件描述符集。

 

Q6:关闭一个已经注册到一个epoll实例的文件描述符会自动被epoll实例移除吗?

A6:是的,但是请注意一下几点。一个文件描述符只是引用了一个文件描述(open(2)),当这个文件描述符通过:dup(2),dup2(2), fcntl(2) F_DUPFD, fork(2),这几种方式复制后,那么新的文件描述符实际上也是引用着同一个文件描述。这个文件描述直到所有引用其的文件描述符关闭之后才会关闭。只有当所有的引用着同一个文件描述的所有的文件描述符关闭之后,才会将注册到epoll实例的文件描述符移除。当然你也可以显式的使用带EPOLL_CTL_DEL参数的epoll_ctl(2)函数。这意味着:尽管注册到epoll的文件描述符的关闭了,但是其他引用同一个文件描述的文件描述符还存在的话,事件还是有可能会报告给epoll。

 

Q7:如果多于一个事件发生了,那么在调用epoll_wait(2)时,那么是一起报告还是分开报告。

A7:一起报告。

 

Q8:对文件描述符的操作会影响已经收集但是还没有报告的事件么?

A9:你可以对一个文件操作符做两个操作:移除的话在这样的情况下就没有讨论的意义了。如果是修改,那么会再次去从可用的I/0里面读取。

 

Q9:在边缘模式下,我需要不断地去读或者直到收到一个EAGAIN吗?

A9:当从epoll_wait(2)得到一个事件的时候,这只是建议你这个文件描述符已经准备好了所要求监控的I/0事件。你必须要意识到他现在是准备好了的,但是下一次read/write有可能产生EAGAIN。那么何时或者如何使用这个文件描述符完全由你自己决定。

 

对于报文或者token-oriented files(比如数据报socket, 以规范模式中止)唯一能够确定其缓冲区的数据是否被完全读完或者写完的方式就是使再去读或者写直到返回EAGAIN。

 

对流定向的文件(如:管道、FIFO、streamsocket)。目前这个read/write缓冲区的状态(是否被读完、写完),你是可以通过检测针对这个文件描述符读的数量或者是写的数量来判断的。比如,如果你调用read去读一定量的数据,但是只是返回给你了很少的字节。那么可以肯定这个读的缓冲区内的数据已经被读完了。类似的事情发生你去写。(如果你不能保证被监控的文件描述符一定是一个流定向的文件,那么不要使用前面所述的方法。)

 

8          可能的陷阱以及如何避免饥渴(边缘模式)

如果有一块I/O 空间,有可能会耗光这些资源导致其他的文件无法运行,因此导致了饥渴的发生。这并不是只是针对epoll。

 

解决方法是维护一个已经准备好了的列表,并且在这些文件描述符的数据结构中标记哪些是已经准备好了的。因此虽然应用知道哪些文件需要被操作,但是仍然需要去遍历所有的已经准备好了的文件。这样同样会导致你接收不到那些已经准备好了的文件的后面的事件。

 

9          如果使用事件缓冲

如果你打算使用事件缓冲,并且打算存储所有从epoll_wait(2)返回的文件描述符,那么请确认你提供了一种方式能够记录是否关闭(比如这也许是由于之前的事件导致的)。假设你从epoll_wait(2)收到了100个事件,那么比如47号文件描述符的事件可以导致13号文件描述符关闭。那么移除了数据结构和关闭了13号文件描述符,但是你的事件缓冲里面仍然有另一个事件还是去处理13号文件描述符,这将导致你困惑。

另外一个对于这样一个调用的解决方案就是当47号文件描述符的事件在运行过程中,使用epoll_ctl(EPOLL_CTL_DEL) 去删除13文件描述符和close(2),并且对其相关数据结构做一个提示移除的标记,并且将其链接到清除列表。当另一个事件还是去处理13号文件描述符时,那么你会发现这个13号文件描述符已经在之前被移除了,这样你就不会困惑了。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值