Handler唤起的基础--eventfd和epoll
熟悉Android应用的我们都知道,每个应用都有一个主线程来处理所有的UI业务,主线程会启用一个Looper用于循环从MessageQueue中获取消息,而这个死循环之所以不会导致CPU满载,就是他有独特的阻塞机制。当没有可执行的消息时,主线程就会陷入阻塞状态并释放CPU,而这套机制就是通过Linux的eventfd和epoll机制实现的。
eventfd
eventfd是Linux中提供的用于事件通知的一种文件描述符,是可以用来实现wait/notify的一种工具,用其可以轻易实现线程间或进程间的事件通知。Looper就是通过eventfd来实现的事件通知。
eventfd内部维护了一个uint64_t类型的一个计数器,对它读写也只能是对其内部计数器进行读写,甚至可以理解为eventfd就是一个数字,对它读写就是对这个数字的赋值和取值。其计数器为0的时候表示文件不可读,为最大值的时候,表示不可写。对eventfd的写入,实际上并不是一个简单的赋值操作,而是累加操作。例如其内部计数器为1的时候,再写入一个3,此时其内部计数器则会为4。因此当计数器为最大值时,就不可再写入了,因为写入会导致溢出。
创建文件描述符
1 | |
该方法用于创建一个文件描述符,其第一个参数是计数器的初始值,第二个参数是对应的flag,可以用‘或’操作设置多个flag。返回值则是该文件描述符。flag取以下三个值:
- EFD_NONBLOCK 使用该
flag创建的文件描述符,对其读写的时候不会阻塞。默认情况下,对eventfd的读写操作是阻塞式的,即eventfd不可读写的时候,线程会阻塞在read、write方法中。而设置了该flag后,若是文件不可读写,则不会阻塞而是直接返回-1。 - EFD_SEMAPHORE 使用该
flag创建的是属于信号量类型的文件描述符。默认情况下,对eventfd的读取操作,会直接读取到内部的计数器的值,读取后重置计数器为0,此时eventfd被置为不可读的状态。而设置了该flag后,读取的时候每次都只能读取到1,并且会将内部计数器值减1,若此时计数器仍不为0,则还是属于可读的。 - EFD_CLOEXEC 当通过exec执行其他程序后,将复制的
eventfd自动关闭。
标准读写
1 | |
对eventfd的读写就像是对普通文件描述符的读写一样没什么特殊的,但是对其读写的大小只能是uint64_t的长度。例如写入内容的话,只能写入uint64_t类型的数字,读取的话,也只能通过uint64_t类型进行接收,否则会读取或写入失败。返回值也是一样的,若是读写成功,则返回读写的大小,即sizeof(uint64_t),失败则返回-1。
默认情况下,读写都是阻塞式的。例如eventfd是不可读的时候,即内部计数器值为0,此时read方法会阻塞当前线程,直到可读的时候才会返回。可以通过创建eventfd的时候传入EFD_NONBLOCK的flag,此时则是非阻塞式的,若是不可读写,则并不会阻塞而是直接返回-1。
eventfd封装的读写
1 | |
除了标准read、write读写,eventfd还有一种读写的方式,即上述的两个方法,实际上也是对标准读写的封装。由于eventfd要求读写的类型必须是uint64_t类型,所以直接使用标准读写可能会出现长度类型的问题。而经过eventfd封装后的读写方法,明确指明了数据类型为eventfd_t类型(实际也是uint64_t类型),避免了写入数据错误的问题。返回值也做了修改,读写成功的话返回值为0,否则为-1。
使用方式示例
1 | |
上述代码实现了在主线程循环读取数据,若是eventfd不可读的话,则会阻塞在read方法中。然后创建了一个子线程每隔1秒会向eventfd中写入一个值,写入后主线程会被唤醒,然后读取到这个值后打印并再次阻塞在read中等待。

小结
eventfd是用来实现wait/notify机制的,因此不要把它当成一个生产者消费者模型,即不要想着一个线程读取,多个线程写入,由于eventfd计数器的特性,每次写入的值会被累加,所以不适合多个线程同时写入。多个线程情况下,可能会出现写入多次,而只读取一次的情况。而若是想实现写入几次读取几次的话,则需要创建时传入EFD_SEMAPHORE的flag,但这样的话,每次读取的时候都只能读取到1,就无法区分线程写入的内容了。总而言之,eventfd的最佳使用环境就是睡眠唤醒。
epoll
根据前面了解的eventfd,已经可以很容易实现Looper了。因为netive层的Looper就是一个简单的睡眠唤醒机制的实现,而eventfd本身来实现这个机制的。所以通过对eventfd包装一下就可以实现一个简单的Looper了。但eventfd还有一些缺陷,当一个线程阻塞在read方法中时,会一直阻塞下去,直到另一个线程写入内容。而我们还有超时的诉求,即read的时候设定一个超时时间,若是阻塞的时间超过了超时时间,则也唤醒的当前线程。这是考虑到我们会发送延迟消息,所以在延迟的时间到达后唤醒线程去处理事件。
epoll是一种事件通知机制,可以用来监控多个fd的读写状态。如前面了解的eventfd,若是存在多个eventfd,有一个线程的需求是监听这多个fd,不论哪个可读的时候,都去处理某些事件。我们就很难去做到上述的场景,或许可以创建eventfd的时候将其设置为非阻塞式的,然后循环read这几个fd,若是有可读的就去处理事件,否则继续循环读取。但这样实现有很大的问题,首先是CPU的问题,由于read是非阻塞式的,会导致程序在fd都不可读的时候一直处于循环中,耗费大量的CPU资源。其次,若是fd不是当前线程打开的,即无法控制其read是否是阻塞式的,上述方案也是不可行的。
epoll就是用来应对这种情况的,使用epoll可以同时监听多个fd,当添加其中的fd可读或者可写后就会唤醒epoll。同时也可以设置超时时间,超时后也会返回。
创建epoll
1 | |
创建epoll有两个方法,第一个方法传入了size的大小,表示epoll可以监听的fd的个数,目前已经弃用,若仍使用的话,size会被舍弃,但是为了兼容旧版本,size的值不能是复数。第二个方法创建epoll可以传入flag,可以传入EPOLL_CLOEXEC.
操作注册fd
1 | |
epoll增加、删除、修改fd都是通过epoll_ctl去操作的,其中参数__epfd是epoll实例,而参数__op代表具体的操作,是定义的三个常量,分别是EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD,即当前操作是增加还是删除或者修改。第三个参数是需要操作的文件描述符。
第四个参数epoll_event是当前要操作的文件描述符的事件,跟文件描述符绑定的。
响应事件
1 | |
事件收到之后是以epoll_event的形式来返回的,如上,其有两个字段。events代表的是监听的fd的事件操作,定义了一堆的枚举量,最常用的就是EPOLLIN(可读时触发)和EPOLLOUT(可写时触发)。第二个字段data是携带的参数,用于辨别当前触发的是哪个文件描述符。通常会将data.fd赋值为对应的fd。当epoll唤醒的时候,会将注册时传的event返回回来,需要通过event去判读是哪个fd唤醒的。
注册事件枚举
1 | |
EPOLL_EVENT是注册fd的时候需要设置的事件,用于设置当前注册的fd可以如何被触发唤醒。若是设置为了EPOLLIN,当文件可读的时候,会唤醒EPOLL。若是设置了EPOLLOUT,则当fd可写时唤醒EPOLL。
事件中还有一个比较重要的值,EPOLLET,即将触发方式设置为ET模式。EPOLL支持两种触发模式,level-triggeed(LT)模式和edge-triggered(ET)模式,EPOLL默认是LT模式。
LT
fd只要可读,就一直触发可读事件。fd只要可写入,就会一直触发可写事件。因此若是fd可读,需要将缓存区中的所有内容都读完,否则下次循环还会触发可读事件。同理,可写时也是如此。
ET
可读事件触发方式:
fd从不可读变为可读时;fd缓存区内容增多时,即缓存区已有内容,然后又被写入了其他内容的时候会触发;fd缓存区不为空,并且通过EPOLL_CTL_MOD修改后出触发。可写事件的触发方式同理一样。
等待返回
1 | |
epoll_wait是阻塞式的,该方法会等待注册的fd进行唤醒。例如注册的fd是EPOLLIN,那么当fd可读的时候,epoll_wait就会返回。第一个参数是epoll实例,第二个参数是一个数组,其中包含的是唤醒的fd对应的poll_event,因为epoll是同时监听多个fd的。第三个参数是最大的event个数,通常是events数组的长度。最后一个参数是超时时间,单位毫秒,超过该时间后会直接唤醒,不论是否有fd注册的条件达到,设置为-1的话表示没有超时时间。
返回值是唤醒的fd的个数,此时会将fd对应的event写入events数组中,从第一个开始写入。因此唤醒后,需要根据返回值去遍历events,然后查看是哪个fd唤醒的。
使用示例
1 | |

小结
epoll类似于一个管家,它可以管理着多个文件描述符,当文件描述符的状态触发某个条件后,就会直接唤醒eoll的等待。epoll可以添加多个文件描述符,并且支持多种类型的fd,不仅仅是eventfd,像file、socketfd、pipe等都是可以的,并且可以设置超时唤醒。
总结
前面介绍了eventfd和epoll的功能和使用方式,了解了这两个东西,后面再去看Android的Looper的时候就能理解它是如何实现的了。Binder驱动是Android中非常重要的一个东西,跨进程通信全靠它,它也是支持epoll机制的,ServiceManager中就是使用epoll机制来监听binder驱动的fd是否有消息的。
