介紹一下 Android Handler 中的 epoll 機制?
目錄:
- IO 多路復用
- select、poll、epoll 對比
- epoll API
- epoll 使用示例
- Handler 中的 epoll 源碼分析
IO 多路復用
IO 多路復用是一種同步 IO 模型,實現一個線程可以監視多個文件句柄。一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作,沒有文件句柄就緒時會阻塞應用程序,交出 cpu。
與多進程和多線程技術相比,IO 多路復用技術的最大優勢是系統開銷小,系統不必為每個 IO 操作都創建進程或線程,也不必維護這些進程或線程,從而大大減小了系統的開銷。
select、poll、epoll 就是 IO 多路復用三種實現方式。
select、poll、epoll 對比
select 最大連接數為進程文件描述符上限,一般為 1024;每次調用 select 拷貝 fd;輪詢方式工作時間復雜度為 O(n)
poll 最大連接數無上限;每次調用 poll 拷貝 fd;輪詢方式工作時間復雜度為 O(n)
epoll 最大連接數無上限;首次調用 epoll_ctl 拷貝 fd,調用 epoll_wait 時不拷貝;回調方式工作時間復雜度為 O(1)
epoll API
int?epoll_create(int?size);
創建 eventpoll 對象,并將 eventpoll 對象放到 epfd 對應的 file->private_data 上,返回一個 epfd,即 eventpoll 句柄。
int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event)?//返回值:成功?0;失敗?-1
對一個 epfd 進行操作。op 表示要執行的操作,包括 EPOLL_CTL_ADD (添加)、EPOLL_CTL_DEL (刪除)、EPOLL_CTL_MOD (修改);fd 表示被監聽的文件描述符;event 表示要被監聽的事件,包括:
- EPOLLIN(表示被監聽的fd有可以讀的數據)
- EPOLLOUT(表示被監聽的fd有可以寫的數據)
- EPOLLPRI(表示有可讀的緊急數據)
- EPOLLERR(對應的fd發生異常)
- EPOLLHUP(對應的fd被掛斷)
- EPOLLET(設置EPOLL為邊緣觸發)
- EPOLLONESHOT(只監聽一次)
int?epoll_wait(int?epfd,?struct?epoll_event?*events,?int?maxevents,?int?timeout)?//返回值:監聽到的產生的事件數
等待 epfd 監聽的 fd 所產生對應的事件。epfd 表示 epoll句柄;events 表示回傳處理事件的數組;maxevents 表示每次能處理的最大事件數;timeout:等待 IO 的超時時間,-1 表示一直阻塞直到來 IO 被喚醒,大于 0 表示阻塞指定的時間后被喚醒
epoll 使用示例
創建一個管道,使用 epoll 監聽管道讀端,然后進入阻塞:
?int?pipFd[2];
?pipe(pipFd);?//打開管道
?struct?epoll_event?event;
?event.data.fd?=?pipFd[0];?//設置為監聽管道讀端
?event.events?=?EPOLLIN?|?EPOLLET;?//設置參數,接收可以?read()?的通知
?int?epfd?=?epoll_create(256);?//創建?epoll?對象
?int?res?=?epoll_ctl(epfd,?EPOLL_CTL_ADD,?pipFd[0],?&event);?//添加管道讀端為要監聽的文件描述符
?struct?epoll_event?allEvs[256];
?int?count?=?epoll_wait(epfd,?allEvs,?256,?5000);?//當前線程進入阻塞,等待被喚醒
?for(int?i?=?0;?i?//被喚醒,處理觸發喚醒文件描述符
?????if(allEvs[i].data.fd?==?pipFd[0]?&&?(allEvs[i].events?&?EPOLLIN)){
?????????char?buffer[256];
?????????read(pipeFd,?buffer,?100);?//接收到管道可以進行讀的信號,開始讀取
?????}
?}
在其他線程寫入管道,通知喚醒:
?write(pipFd[1],?str,strlen("hello"));
eventfd
eventfd 是 Linux 系統中一個用來通知事件的文件描述符,基于內核向用戶空間應用發送通知的機制,可以有效地被用來實現用戶空間事件驅動的應用程序。
簡而言之:eventfd 就是用來觸發事件通知,它只有一個系統調用接口:
int?eventfd(unsigned?int?initval,?int?flags);
表示打開一個 eventfd 文件并返回文件描述符,支持 epoll/poll/select 操作。
之所以要在介紹 Handler native 源碼前先介紹 eventfd,是因為在 Android 6.0 后,Handler 底層替換為 eventfd/epoll 實現。而 6.0 之前是由 pipe/epoll 實現的,就像上面的 epoll 使用示例那樣。
Handler 中的 epoll 源碼分析
主要分析 MessageQueue.java 中的三個 native 函數:
private?native?static?long?nativeInit();?//返回?ptr
private?native?void?nativePollOnce(long?ptr,?int?timeoutMillis);?//阻塞
private?native?static?void?nativeWake(long?ptr);?//喚醒
nativeInit
首先來看 nativeInit 方法,nativeInit 在 MessageQueue 構造函數中被調用,其返回了一個底層對象的指針:
????MessageQueue(boolean?quitAllowed)?{
????????mQuitAllowed?=?quitAllowed;
????????mPtr?=?nativeInit();
????}
對應實現在 android_os_MessageQueue.cpp 中:
static?jlong?android_os_MessageQueue_nativeInit(JNIEnv*?env,?jclass?clazz)?{
????NativeMessageQueue*?nativeMessageQueue?=?new?NativeMessageQueue();
????...
????return?reinterpret_cast(nativeMessageQueue);
}
可見 MessageQueue 對應的底層對象就是 NativeMessageQueue,而 NativeMessageQueue 初始化時會創建一個底層的 Looper 對象:
NativeMessageQueue::NativeMessageQueue()?:
????????mPollEnv(NULL),?mPollObj(NULL),?mExceptionObj(NULL)?{
????mLooper?=?Looper::getForThread();
????if?(mLooper?==?NULL)?{
????????mLooper?=?new?Looper(false);
????????Looper::setForThread(mLooper);
????}
}
如上代碼,可以知道 Looper 對象是 ThreadLocal 類型。Looper 的構造函數如下:
Looper::Looper(bool?allowNonCallbacks)?:
????????mAllowNonCallbacks(allowNonCallbacks),?...{
????mWakeEventFd?=?eventfd(0,?EFD_NONBLOCK?|?EFD_CLOEXEC);
????...
????rebuildEpollLocked();
}
首先通過 eventfd 系統調用返回一個文件描述符,專門用于事件通知。接著來看 rebuildEpollLocked 方法:
void?Looper::rebuildEpollLocked()?{
????mEpollFd?=?epoll_create(EPOLL_SIZE_HINT);
????struct?epoll_event?eventItem;
????memset(&?eventItem,?0,?sizeof(epoll_event));
????eventItem.events?=?EPOLLIN;
????eventItem.data.fd?=?mWakeEventFd;
????int?result?=?epoll_ctl(mEpollFd,?EPOLL_CTL_ADD,?mWakeEventFd,?&?eventItem);?
????...
}
可以看到我們已經熟悉的 epoll 操作了:通過 epoll_create 創建 epoll 對象,然后調用 epoll_ctl 添加 mWakeEventFd 為要監聽的文件描述符。
nativePollOnce
之前學習 Handler 機制時多次看到過 nativePollOnce 方法,也知道它會進入休眠,下面就來徹底搞懂它的原理。對應的底層調用同樣是在 android_os_MessageQueue.cpp 中:
static?void?android_os_MessageQueue_nativePollOnce(JNIEnv*?env,?jobject?obj,
????????jlong?ptr,?jint?timeoutMillis)?{
????NativeMessageQueue*?nativeMessageQueue?=?reinterpret_cast(ptr);
????nativeMessageQueue->pollOnce(env,?obj,?timeoutMillis);
}void?NativeMessageQueue::pollOnce(JNIEnv*?env,?jobject?pollObj,?int?timeoutMillis)?{
????mLooper->pollOnce(timeoutMillis);
????...
}
可以看到實現同樣是在 Looper.cpp 中,接著來看 Looper 的 pollOnce 方法:
int?Looper::pollOnce(int?timeoutMillis,?int*?outFd,?int*?outEvents,?void**?outData)?{
????for?(;;)?{
????????...
????????result?=?pollInner(timeoutMillis);
????}
}
int?Looper::pollInner(int?timeoutMillis)?{
????...
????struct?epoll_event?eventItems[EPOLL_MAX_EVENTS];
????int?eventCount?=?epoll_wait(mEpollFd,?eventItems,?EPOLL_MAX_EVENTS,?timeoutMillis);
????...
至此通過調用 epoll_wait 方法,當前線程進入休眠,等待被喚醒。
nativeWake
最后來看如何通過 nativeWake 喚醒線程,首先是 android_os_MessageQueue.cpp 中:
static?void?android_os_MessageQueue_nativeWake(JNIEnv*?env,?jclass?clazz,?jlong?ptr)?{
????NativeMessageQueue*?nativeMessageQueue?=?reinterpret_cast(ptr);
????nativeMessageQueue->wake();
}void?NativeMessageQueue::wake()?{
????mLooper->wake();
}
與 nativeInit、nativePollOnce 一樣,最終實現都是在 Looper.cpp 中,Looper 的 wake 方法如下:
void?Looper::wake()?{
????uint64_t?inc?=?1;
????ssize_t?nWrite?=?TEMP_FAILURE_RETRY(write(mWakeEventFd,?&inc,?sizeof(uint64_t)));
????if?(nWrite?!=?sizeof(uint64_t))?{
????????if?(errno?!=?EAGAIN)?{
????????????LOG_ALWAYS_FATAL("Could?not?write?wake?signal?to?fd?%d:?%s",
????????????????????mWakeEventFd,?strerror(errno));
????????}
????}
}
其中關鍵邏輯是對 mWakeEventFd 發起寫入操作,從而喚醒 nativePollOnce 中通過 epoll_wait 進入休眠的線程。
推薦閱讀:開源一組視頻時間軸控件
Activity Window 創建及添加過程
抽象工廠模式
Android UI 繪制請求與繪制時機工廠模式Android 消息屏障與異步消息Java 并發編程知識點梳理總結
關注我
助你升職加薪
Android?面試官

