这是一个开篇,包含前言和内容,内容部分讲了一点关于 handler 的例子。
首先,Linux 内核是非常优秀的程序,同时,Linux 是一个优秀的操作系统,既然是操作系统,那么
关于操作系统的知识,Linux 内核都会多少涉及到。
本人是一年的小小开发仔,从个人角度来说,Linux 内核是很值得一读学习的知识,结合相关的书籍,在
网站上下载代码,解压之后用 source insight,有时间瞧一瞧感兴趣的部分,挺不错的。比如呢:
1、当使用 java 编程的时候,遇到了使用到 ReentrantReadWriteLock 对于临界区进行必要的操作,
ReentrantReadWriteLock 的原理大家都比较清楚,这个时候,可能会更想知道临界区是啥?
linux 对于临界区的各种操作分别有什么?
2、当使用 handler 进行消息发送的时候,可能主线程阻塞甚至产生 ANR,这时候看下 traces 文件,可能发现
最后都走到了 nativePollOnce 方法,这是 MessageQueue 里面 的 next() 里面的一个调用的方法,sdk 很清楚地
注释了next() 方法 may be bocked,那么,此时就可以去探索,nativePollOnce 里面使用了 linux 的 epoll,多
路IO复用的相关知识。
3、当使用 LRUCache的时候,显而易见,LRUCache 的原理都比较清楚,最近最少使用的知识也是操作系统的范
畴,那么可以联想到,在 Linux 内核中,使用了 LRU 链表 来实现这个算法,当然其中也涉及到了链表数据结构,
这也会让我们进一步探索。
4、当阅读线程池的源码时候,可能会感慨线程池的设计如此厉害,更多的可以很自然而然联想到,线程池架构与
CPU 三级缓存的分层的联系十分相似。
5、volatile 关键字保证可见性,那么我们也可以联系到 高速缓冲区、主存等等概念,更多可以联想到内核同步的内存屏障、
读写自旋锁等。
6、在分析 Android 的 Binder 通信的时候,可能你会更想知道Linux内核有什么通信方式,FIFO、Socket 等等,
进而又涉及到了队列的知识以及生产者消费者的问题,各种思想总是相通的。
7、在学习 java jvm 的知识时候,关于堆的管理等等,也许可能你进一步会去探索到树的知识等等
8、在学习 数据库的时候,可能会探索 B-Tree 和 磁盘的关系等等
。。。。。。
当然,也可能是在设计一个 SDK、或者做一些 缓存优化,当然甚至是做一个自定义 View,甚至学习三次握手的半
连接队列等等知识,Linux 内核我认为都是挺有帮助的,可能是比较直接可以借鉴,或者是潜移默化的。
萝莉八所了这么多前言 。。。总的来说,可以涉及到操作系统的知识,并且作为用户态和内核态的切换,又
涉及到了分层的思想,另外,关于各种数据结构的使用、缓存思想、内存管理等等都很值得学习。
接下来的内容涉及的是 Handler的知识,主要是里面运用了 epoll 的知识。
Handler 作为一套通信框架,在 Android 中的使用十分广泛,在 ActivityThread.java 里面就通过 Handler 来
处理各种消息类型,包括Activity的启动等等 。个人观点,Android 是基于消息驱动的,那Handler的重要地位不
言而喻 。
Handler 的流程图 ( 画工不好~~):
画图工具 :http://asciiflow.com/
接着,带着一个比较常见的问题看下Handler。
postDelay 是 怎么工作的呢?
Handler 使用有很多方式,常用的发送消息最终都是调用了 sendMessageAtTime:
最后调用的是 MessageQueue.java 的 enqueueMessage 方法:
上面的 入队方法主要是来遍历链表,target 对象是我们在发送一条消息的时候,handler 将当前对象赋值给 Msg 的 target,即:
msg.target = this;
所以一个 Handler 发送多条消息都会被放到同一个线程的队列中中,在 Looper 中可以看到:
可以看到,每一个 Looper 都和当前线程绑定,得益于 ThreadLocal,所以在哪个线程实例化的 Handler,发送消息的时候,msg 就会被对应线程的 MessageQueue 入队,并且msg 的 target 都是执行发送的 Handler。
当 一个 Handler 发送多条 msg 时候,可以看到上面,用了一个无限循环对链表进行遍历,根据是否延时发送来调整节点位置,
所以 postDelay 不同的只是调用 sendMessageAtTime 的时候需要将 (延时的时长 + 当前时间戳) ,再赋值给 msg 的 when
上面注释还有一个 同步分割栏 barrier 的概念,
在 MessageQueue.java 中,可以通过调用 postSyncBarrier 和 removeSyncBarrier 来分别插入和移除一条同步分割栏:
用 token 来作为标识,从链表移除:
接下来看下,MessageQueue 里面的next() 方法:
其中,里面的 flushPendingCommands 将 当前线程挂起的 Binder 指令刷新到内核驱动
到此都是 java 层的分析,上面的 Handler 的流程图实际上还少了一点:
接下来看下 nativePollOnce 方法
nativePollOnce 是一个 native 方法,具体在
frameworks/base/core/jni/android_os_MessageQueue.cpp 调用:
同样,在同一个 cpp 文件中,也可以看到 nativeWake 的调用:
可以看到,nativePollOnce 和 nativeWake 最终分别调用了 mLooper 中的 pollOnce 和 wake 方法
mLooper 是 Looper.cpp 的实例,位于
system/core/libutils/Looper.cpp
具体看下 pollOnce 方法
外部将 timeoutMillis 传递进来,并且最后调用了 pollInner(int timeoutMillis)
int Looper::pollInner(int timeoutMillis) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);
#endif
// Adjust the timeout based on when the next message is due.
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - next message in %" PRId64 "ns, adjusted timeout: timeoutMillis=%d",
this, mNextMessageUptime - now, timeoutMillis);
#endif
}
// Poll.
清空操作
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
// We are about to idle.
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
调用 epoll_wait 方法
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// No longer idling.
mPolling = false;
// Acquire lock.
mLock.lock();
// Rebuild epoll set if needed.
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked();
goto Done;
}
// Check for poll error.
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
ALOGW("Poll failed with an unexpected error, errno=%d", errno);
result = POLL_ERROR;
goto Done;
}
// Check for poll timeout.
if (eventCount == 0) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - timeout", this);
#endif
result = POLL_TIMEOUT;
goto Done;
}
// Handle all events.
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - handling events from %d fds", this, eventCount);
#endif
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd) {
if (epollEvents & EPOLLIN) {
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;
// Invoke pending message callbacks.
mNextMessageUptime = LLONG_MAX;
while (mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
if (messageEnvelope.uptime <= now) {
// Remove the envelope from the list.
持有一个Handler的强引用,直到调用 handleMessage 结束,然后移除掉
// We keep a strong reference to the handler until the call to handleMessage
// finishes. Then we drop it so that the handler can be deleted *before*
// we reacquire our lock.
{ // obtain handler
sp handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS
ALOGD("%p ~ pollOnce - sending message: handler=%p, what=%d",
this, handler.get(), message.what);
#endif
handler->handleMessage(message);
} // release handler
mLock.lock();
mSendingMessage = false;
result = POLL_CALLBACK;
} else {
// The last message left at the head of the queue determines the next wakeup time.
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
// Release lock.
mLock.unlock();
// Invoke all response callbacks.
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS
ALOGD("%p ~ pollOnce - invoking fd event callback %p: fd=%d, events=0x%x, data=%p",
this, response.request.callback.get(), fd, events, data);
#endif
// Invoke the callback. Note that the file descriptor may be closed by
// the callback (and potentially even reused) before the function returns so
// we need to be a little careful when removing the file descriptor afterwards.
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
removeFd(fd, response.request.seq);
}
// Clear the callback reference in the response structure promptly because we
// will not clear the response vector itself until the next poll.
response.request.callback.clear();
result = POLL_CALLBACK;
}
}
return result;
}
这个方法代码有点长,大部分可以看里面的英文注释,可以看到 pollInner 里面 调用了 kernel 的 epoll_wait
同时可以看到 pollInner 里面调用了一个 rebuildEpollLocked(); 方法,
可以看到,里面调用了 epoll_create 和 epoll_ctl,到此,可以看到,有 epoll 关键字的已经有三个方法了,分别是:
epoll_create、epoll_ctl 、epoll_wait
接下来把目光移到 epoll 的相关 tips:
epoll 模型:
epoll是Linux 2.6中引入的新系统调用,旨在替换 select 和 poll。与早期的系统调用(O(n))不同,epoll是一种O(1)算法,这意味着随着观察文件描述符数量的增加,它可以很好地扩展。对于:select使用线性搜索通过监视文件描述符列表,结果耗时O(n),而epoll使用的是内核文件结构中的回调。
epoll 的 API 很简洁,如下:
或者移步这里:
epoll_create:http://www.man7.org/linux/man-pages/man2/epoll_create.2.html
epoll_ctl : http://www.man7.org/linux/man-pages/man2/epoll_ctl.2.html
epoll_wait:http://www.man7.org/linux/man-pages/man2/epoll_wait.2.html
int epoll_create(int size);
创建一个epoll对象并返回其文件描述符,flags参数允许修改epoll行为,
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
控制(配置)此对象监视哪些文件描述符,以及哪些事件。 参数 op 可以是ADD,MODIFY 或 DELETE 。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待epoll_ctl 注册的任何事件,至少有一次事件触发或发生超时的时候,就会返回。 最多返回maxevents 。
一般来说,步骤有三:
一般说到 epoll,免不了和 select 来做一个比较,直观的看,select O(n) 的时间复杂度对于高并发的操作肯定不能接受,而 epoll 的高效率使用借助了链表和红黑树,当我们执行epoll_create时,会创建 epoll 对象,并且创建了红黑树和链表,在需要的时候对此对象进行操作,如执行epoll_ctl,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在插入到红黑书中,然后向内核注册回调函数,当中断事件来临时向链表中插入数据,后续执行epoll_wait时返回 链表里的数据即可,这样避免了轮询的耗时和阻塞。
通常一条有延时的消息 msgA ,delay 200ms,入队的时候会阻塞,那么队列下一次唤醒的时机:
A:有一条新的消息入队
B:没有新的消息入队,200ms之后执行延时的消息
那么,没有新消息入队的时候,它怎么知道要200ms之后执行呢?
在调用epoll_create时,除了在内核缓存里建了个红黑树用于存储以后 epoll_ctl 传来的套接字,会再建立一个list 链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可,有的话会马上,没有的会进行休眠 , 直到timeout 时间到后即使链表没数据也返回。
ok,epoll 的 tips 到此~~
那么,如果要模仿实现一个 Handler 呢,用 java 的话我们可以用 notify、wait 或者 Lock 等来模拟,此处就不展开了。
捋一下:本文涉及到了如下知识点:
关于epoll,有兴趣可以阅读下面的文章:
http://man7.org/linux/man-pages/man7/epoll.7.html
https://kovyrin.net/2006/04/13/epoll-asynchronous-network-programming/
https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/
这是Handler 作为例子引出的 Linux 开篇,涉及了epoll,水平有限,内容可能有很多纰漏,欢迎指正。
ps:很多代码注释都很有用,比如 Scroller.java,类开头就将 Scroller 的使用三步曲都写出来了,比如 Binder.java,开头解释了 Binder 的挂起和跨进程通信,类似的还有 RecyclerView 啊等等。