1.Linux epoll
epoll全称eventpoll(poll译为投票数、计票),是Linux内核中的一种可扩展IO事件处理机制,能够提高应用程序同时处理大量IO操作请求时的性能,它是Linux I/O多路复用的一个实现(除了epoll外,还有select和poll)。
一般当系统同时发起多个IO事件请求时,Linux需要轮询所有事件,处理每个事件对应的事件流。这样就会造成系统中链接Linux内核的IO事件太多,每一次轮询都需要耗费大量的时间和资源。
使用epoll_wait机制后,epoll会把每一个流对应的IO事件通知内核,这样内核就能根据具体的IO事件去处理对应的stream流。
①Linux I/O多路复用
在Linux中,任何可以进行I/O操作的对象都可以看做是流,比如一个文件、socket、pipe都可看作流。通过调用read()可以从流中读出数据,通过write()可以往流中写入数据。
假定现在需要从流中读数据,但是流中还没有数据,比如客户端要从socket中读数据,但是服务器还没有把数据传回来:
int socketfd = socket();
connect(socketfd,serverAddr);
int n = send(socketfd,'在吗');
n = recv(socketfd); //等待接受服务器端发来信息
由于服务器还没有把数据传回来,这时可以有两种处理办法:
1)阻塞: 线程阻塞在recv()方法,直到读到数据后再继续向下执行;
2)非阻塞:recv()方法没读到数据就立刻返回-1,用户线程按照固定间隔轮询recv()方法,直到有数据返回。
在阻塞模式下一个线程一次只能处理一个流的I/O事件,想要同时处理多个流就需要使用多线程 + 阻塞I/O的方案。但是每个socket都对应一个线程会造成很大的资源占用,尤其对于长连接来说,线程资源一直不会释放,如果后面有很多连接的话,很快就会把机器的内存跑完。
在非阻塞模式下,单线程可以同时处理多个流,只要不停的把所有流从头到尾访问一遍,就可以得知哪些流有数据(返回值大于-1),但这样的做法效率也不高,因为如果所有的流都没有数据,就只会白白浪费CPU。
所以,只有阻塞和非阻塞这两种方案时,一旦有监听多个流事件的需求,用户程序只能选择要么浪费线程资源(阻塞型I/O),要么浪费CPU资源(非阻塞型I/O),没有其他更高效的方案。并且这个问题在用户程序端是无解的,必须让内核创建某种机制,把这些流的监听事件接管过去,因为任何事件都必须通过内核读取转发,内核总是能在第一时间知晓事件发生。
因此出现了I/O多路复用机制,它能够让用户程序“同时监听多个流读写事件”。
②epoll
epoll提供了三个函数:
int epoll_create(int size); 用于创建一个epoll池,就是在内核缓冲区开辟一块空间,用来存放需要监听的文件描述符fd,当文件描述符发生IO读写事件变化时,会在内核层面进行状态的查询,最终通知用户空间程序有数据变化。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,负责向epoll对象中添加、修改或删除文件描述符fd,返回0表示成功,–1表示失败,失败时需要根据errno错误码判断错误类型。最后一个参数event告诉内核需要监听什么事件。
比如网络请求,socketfd监听的就是可读事件,一旦接收到服务器返回的数据,监听socketfd的对象将会收到回调通知,表示socket中有数据可以读了。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 等待事件的产生,是使用户线程阻塞的方法。
第1个参数epfd是epoll的描述符。
第2个参数events用来从内核得到事件的集合,epoll会把发生的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会帮助我们在用户态中分配内存。内核这种做法效率很高)。
第3个参数maxevents告诉内核这个events有多大,表示本次可以返回的最大事件数目。maxevents的值不能大于创建epoll_create()时的size。
第4个参数timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果timeout为0表示立刻返回不会等待;为-1则一直阻塞,直到有数据返回。该函数返回值表示需要处理的事件数目,如果返回0表示已超时;如果返回–1则表示出现错误,需要检查errno错误码判断错误类型。
所以,epoll_wait监听通过epoll_ctl函数添加文件描述符的IO事件变化,如果这些文件描述符没有发生IO事件,那么当前线程就会在epoll_wait函数中进入阻塞,阻塞时间由timeoutMillis来指定。如果从epoll_wait获取到了返回eventCount并且返回值大于0,说明有其他线程通过pipe的写端文件描述符或eventfd的mWakeEventFd文件描述符向epoll_create函数创建的内核缓存区中写入了数据。 然后最终就会沿着调用链回到Java层,获取到msg消息对象。
③Linux eventfd
eventfd专门用来传递事件的fd,它提供的功能非常简单,就是累计计数。
int efd = eventfd();
write(efd, 1);//写入数字1
write(efd, 2);//再写入数字2
int res = read(efd);
printf(res);//输出值为 3
通过write()函数可以向eventfd中写入一个int类型的值,并且只要没有发生读操作,eventfd中保存的值将会一直累加。
通过read()函数可以将eventfd保存的值读出来,并且在没有新的值加入之前,再次调用read()方法会发生阻塞,直到有人重新向eventfd写入值。
eventfd实现的是计数的功能,只要eventfd计数不为0,就表示fd是可读的。再结合epoll的特性,可以非常轻松的创建出生产者/消费者模型。epoll + eventfd作为消费者大部分时候处于阻塞休眠状态,而一旦有请求入队(eventfd被写入值),消费者就立刻唤醒处理。
Handler的阻塞机制底层逻辑就是利用的epoll + eventfd。
2.Handler阻塞机制
从Android 2.3开始,Google把Handler的阻塞/唤醒方案从Object#wait()/notify()改成了用Linux epoll来实现。原因是Native层也需要引入一套消息管理机制用于提供给C/C++开发者使用,而现有的阻塞/唤醒方案是为Java层准备的,只支持Java。因此从Android 2.3开始在Native层重新实现了一套阻塞/唤醒方案,Java层弃用Object#wait()/notify(),改为通过jni调用Native进入阻塞态。
看一下Handler是如何调用jni的。
Android中消息处理机制由4部分组成:Message、Looper、MessageQueue和Handler。其中MessageQueue用来描述消息队列,Looper用来创建消息队列以及开启消息循环,Handler用来发送和处理消息。
从源码角度来分析:
ActivityThread.java:
public static void main(String[] args) {
…
Looper.prepareMainLooper(); //调用Looper构造函数,并创建MessageQueue消息队列
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq); //会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,具体过程可查看startService流程分析
Looper.loop();//开启消息循环
}
Looper.prepareMainLooper()方法:
public static void prepareMainLooper() {
<