前言
我們知道Android消息機(jī)制是通過Handler、Looper與MessageQueue建立的整個(gè)體系,Looper.loop()方法是開啟了一個(gè)死循環(huán),不停地從MessageQueue中取消息,但是為什么死循環(huán)不會(huì)浪費(fèi)CPU資源呢?這就要開啟我們的EPoll了。
Message next() {
for (;;) {
// 調(diào)用了native pollonce
nativePollOnce(ptr, nextPollTimeoutMillis);
}
}
前提鋪墊
- 同步/異步 IO
同步:執(zhí)行IO操作時(shí),CPU等待,直到IO操作結(jié)束,再繼續(xù)執(zhí)行CPU后續(xù)操作。
異步:執(zhí)行IO操作時(shí),CPU執(zhí)行后續(xù)操作,無需等待IO操作結(jié)束,結(jié)束后通知CPU即可。
形象化表達(dá) 點(diǎn)餐:
同步:去KFC點(diǎn)餐,服務(wù)員說蛋撻還得等20分鐘出爐,那么客戶準(zhǔn)備吃完蛋撻再去逛街,所以就一直等著蛋撻直到蛋撻出爐再去逛街。
異步: 去KFC點(diǎn)餐,服務(wù)員說蛋撻還得等20分鐘出爐,那么客戶準(zhǔn)備吃完蛋撻再去逛街,所以就先去逛街,20分鐘后再來取蛋撻。 - 阻塞/非阻塞IO
理論上,阻塞 = 同步,異步 = 非阻塞
阻塞/非阻塞IO在不同的語境中與同步/異步IO是代表不同的意思的,這就涉及對(duì)IO模型的討論了,這個(gè)我們后續(xù)會(huì)說。 - 用戶空間/內(nèi)核空間
- 緩存IO
緩存IO稱為標(biāo)準(zhǔn)IO,大多數(shù)的文件系統(tǒng)默認(rèn)的IO操作都是緩存I/O。在Linux中,數(shù)據(jù)會(huì)先從磁盤復(fù)制到內(nèi)核控件的緩沖區(qū),然后從內(nèi)核空間緩沖區(qū)復(fù)制到應(yīng)用程序的地址控件。
優(yōu)點(diǎn):
1、緩存IO在一定程度上將用戶空間與內(nèi)核控件進(jìn)行了數(shù)據(jù)分離,確保了系統(tǒng)本身的應(yīng)用安全。
2、減少了讀取磁盤的次數(shù),從而提升性能。
缺點(diǎn):
數(shù)據(jù)在進(jìn)行傳輸?shù)倪^程中,需要多次進(jìn)行應(yīng)用程序地址空間(用戶空間)與內(nèi)核緩存之間進(jìn)行多次數(shù)據(jù)拷貝過程,因?yàn)閼?yīng)用程序地址空間無法直接與磁盤進(jìn)行操作,所以拷貝所帶來的CPU開銷是非常大的。 - 進(jìn)程的阻塞
正在執(zhí)行任務(wù)的進(jìn)程,由于期待的某些時(shí)間還未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某些操作未完成、新數(shù)據(jù)尚未到達(dá)或沒有新的工作等等,則由系統(tǒng)自動(dòng)執(zhí)行Block阻塞原語,使之放棄CPU使用權(quán),移交給其他進(jìn)程,所以不占用CPU資源。 - 文件描述符
文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。
在Linux中,文件描述符在形式上就是一個(gè)非負(fù)整數(shù)。相當(dāng)于一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)該進(jìn)程打開的文件的記錄表。程序中一個(gè)文件對(duì)應(yīng)一個(gè)文件描述符,是內(nèi)核在創(chuàng)建或打開時(shí)返回的。
網(wǎng)絡(luò)數(shù)據(jù) I/O
計(jì)算機(jī)是如何接收網(wǎng)絡(luò)數(shù)據(jù)的呢?我們知道計(jì)算機(jī)網(wǎng)卡是用來進(jìn)行無線通訊的,網(wǎng)卡首先接收到的數(shù)據(jù),通過網(wǎng)卡接口傳輸?shù)接布娐分?,再將?shù)據(jù)寫入到內(nèi)存中某個(gè)地址。其中涉及到DMA(Direct Memory Access)傳輸以及IO通路選擇。
- DMA(Direct Memory Access)
根據(jù)計(jì)組知識(shí),我們知道CPU是通過地址總線,數(shù)據(jù)總線,控制信號(hào)總線來與系統(tǒng)其他部分進(jìn)行數(shù)據(jù)通信的。地址線用于提供內(nèi)存或I/O設(shè)備的地址,即指明需要讀/寫數(shù)據(jù)的具體位置。數(shù)據(jù)線用于在 CPU 和內(nèi)存或 I/O 設(shè)備之間提供數(shù)據(jù)傳輸?shù)耐ǖ溃刂凭€則負(fù)責(zé)指揮執(zhí)行的具體讀/寫操作。所以說32位機(jī)器代表的是內(nèi)部地址線和數(shù)據(jù)線的根數(shù)有32根。那么DMA數(shù)據(jù)傳輸?shù)姆绞?,就是說外設(shè)試圖通過總線向另一個(gè)設(shè)備發(fā)送數(shù)據(jù),并且是大量數(shù)據(jù)的時(shí)候,外設(shè)會(huì)先去請(qǐng)求CPU發(fā)送DMA請(qǐng)求信號(hào)。外設(shè)通過專門的接口電路(DMA控制器),向CPU發(fā)出接管總線控制權(quán),當(dāng)CPU接收到信號(hào)后,在當(dāng)前的總線周期結(jié)束后,會(huì)按照DMA信號(hào)的優(yōu)先級(jí)和提出DMA請(qǐng)求的先后順序進(jìn)行響應(yīng)DMA信號(hào)。CPU會(huì)讓出總線控制權(quán),這時(shí)外設(shè)持有總線控制權(quán),可以直接與存儲(chǔ)器進(jìn)行數(shù)據(jù)通信,不在需要CPU干預(yù),等待數(shù)據(jù)發(fā)送完畢之后,會(huì)向CPU發(fā)送DMA結(jié)束信號(hào),歸還總線控制權(quán)。
中斷信號(hào)
我們?cè)趯W(xué)習(xí)計(jì)組的時(shí)候,一定聽說過CPU中斷信號(hào)。也就是說,若硬件產(chǎn)生的信號(hào),需要讓CPU進(jìn)行響應(yīng),換句話說,硬件產(chǎn)生的信號(hào)一般為中斷信號(hào),因?yàn)樵撔盘?hào)一旦不作出相應(yīng),就會(huì)將數(shù)據(jù)丟失掉。比如,當(dāng)計(jì)算機(jī)收到了斷電信號(hào)時(shí),CPU會(huì)立即去通知保存數(shù)據(jù)的程序,防止數(shù)據(jù)丟失,電容可以保存一些電量用于提供CPU運(yùn)行一小段時(shí)間。那么計(jì)算機(jī)如何知道已經(jīng)接受到了網(wǎng)卡數(shù)據(jù)呢?網(wǎng)卡寫入內(nèi)存以后,網(wǎng)卡會(huì)向CPU發(fā)出一個(gè)中斷信號(hào),操作系統(tǒng)就能夠知道有新數(shù)據(jù)到來,通過網(wǎng)卡中斷程序去處理數(shù)據(jù)。
進(jìn)程阻塞為什么不會(huì)消耗CPU資源?
阻塞的概念我們已經(jīng)理解了,我們先看一段基礎(chǔ)的網(wǎng)絡(luò)編程代碼。
int socket = createSocket()
bindSocket();
listenSocket();
int c = accept(socket,.....);
// 死循環(huán)之類的從socket inputStream 阻塞線程
recv(c,...)
先創(chuàng)建Socket,綁定,監(jiān)聽 和 接受 在通過死循環(huán)接收數(shù)據(jù)。recv 是一個(gè)阻塞方法,當(dāng)程序執(zhí)行recv就一直等到收到數(shù)據(jù)再往下執(zhí)行。
所以操作系統(tǒng)為了執(zhí)行多任務(wù),也就類似進(jìn)程調(diào)度,會(huì)分為運(yùn)行與等待狀態(tài)等等(線程同樣如此)。運(yùn)行就是所謂的獲取到CPU使用權(quán),阻塞就是等待狀態(tài),也就失去CPU使用權(quán)。CPU就是不停地進(jìn)行各個(gè)進(jìn)程間的使用切換,所以由于速度處理的很快,看上去是同時(shí)執(zhí)行多個(gè)任務(wù)。
如何知道哪些任務(wù)要進(jìn)行CPU處理,哪些需要讓出CPU呢?
-
工作隊(duì)列
進(jìn)程運(yùn)行狀態(tài),將會(huì)把該進(jìn)程依次放入到工作執(zhí)行隊(duì)列中,需要cpu執(zhí)行處理的隊(duì)列。
工作隊(duì)列 -
等待隊(duì)列
進(jìn)程一旦處于阻塞狀態(tài),也就是等待狀態(tài)時(shí),比如Socket在執(zhí)行創(chuàng)建語句時(shí),操作系統(tǒng)會(huì)創(chuàng)建一個(gè)由文件系統(tǒng)管理的Socket對(duì)象,這個(gè)對(duì)象包含發(fā)送、接收緩沖區(qū)以及等待隊(duì)列成員。
當(dāng)最終進(jìn)程A執(zhí)行recv函數(shù)的時(shí)候,操作系統(tǒng)會(huì)將進(jìn)程A從工作隊(duì)列移除到等待隊(duì)列中,這時(shí)候工作隊(duì)列只剩下進(jìn)程B、C,之后CPU輪流執(zhí)行進(jìn)程B、C的代碼。所以CPU不會(huì)區(qū)執(zhí)行等待隊(duì)列中的進(jìn)程,因此也就不會(huì)浪費(fèi)CPU資源。
等待隊(duì)列
工作隊(duì)列 - 喚醒進(jìn)程
喚醒進(jìn)程這里,一旦Socket接收到消息,我們之前描述的會(huì)收到一個(gè)中斷信號(hào),告訴CPU接收到了消息 同樣的,又將進(jìn)程A重新放回工作隊(duì)列中。
Epoll在Android中的應(yīng)用
Code from Android Data Source 5.1 -- Looper.cpp
Epoll 初始化
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
// Android 采用pipe通道進(jìn)行數(shù)據(jù)的通信 result = 0成功,失敗返回-1
int result = pipe(wakeFds);
// 管道讀端 fd
mWakeReadPipeFd = wakeFds[0];
// 管道寫端 fd
mWakeWritePipeFd = wakeFds[1];
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
mIdling = false;
// Allocate the epoll instance and register the wake pipe.
// 創(chuàng)建Epoll句柄,返回一個(gè)文件描述符 方法參數(shù)為EPOLL_SIZE_HINT = 8的內(nèi)核監(jiān)聽容量
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
// epoll 事件注冊(cè)函數(shù) 第一個(gè)傳入mEpollFd文件描述符,第二個(gè)參數(shù)代表動(dòng)作,EPOLL_CTL_ADD代表注冊(cè)新的fd到epfd中
// 第三個(gè)參數(shù) 代表監(jiān)聽哪個(gè)文件描述符 也就是說在Android中epoll是通過監(jiān)聽讀端fd,來告知MessageQueue來數(shù)據(jù)了
// 第四個(gè)參數(shù) 用于處理監(jiān)聽什么事件
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
}
其中epoll_ctl()方法中的第二個(gè)參數(shù)代表執(zhí)行操作宏定義,分為三種。
- EPOLL_CTL_ADD
添加新的事件到epoll中 - EPOLL_CTL_MOD
修改epoll中的事件 - EPOLL_CTL_DEL
刪除epoll中的事件
接下來,MessageQueue next()方法實(shí)則是調(diào)用了pollOnce方法,我們來看下。
android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
//.... 省略了大量代碼
// 最終調(diào)用了 pollInner 方法并傳入了超時(shí)時(shí)長
result = pollInner(timeoutMillis);
}
}
int Looper::pollInner(int timeoutMillis) {
// Poll.
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
// We are about to idle.
// 是否是空轉(zhuǎn)狀態(tài) 也就是在調(diào)用epoll_wait時(shí),加入一個(gè)標(biāo)記,表明是否阻塞
mIdling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 執(zhí)行epoll_wait方法進(jìn)行阻塞,等待fd socket 返回?cái)?shù)據(jù)
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// No longer idling.
// 取消空轉(zhuǎn)狀態(tài),表明目前已經(jīng)收到了數(shù)據(jù)
mIdling = false;
// Acquire lock.
// 加鎖用于后續(xù)的fd遍歷
mLock.lock();
// 偽代碼...
// epoll_wait 之后一般是一個(gè)循環(huán)
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
// 若接收到的數(shù)據(jù)來源是正在監(jiān)聽的pipe通道所開辟的fd,則說明當(dāng)前Looper綁定的Thread中的MessageQueue接收到了數(shù)據(jù),
// 可以繼續(xù)走Java代碼中的For循環(huán)之后的代碼了,繼續(xù)獲取CPU執(zhí)行權(quán)。
if (fd == mWakeReadPipeFd) {
if (epollEvents & EPOLLIN) {
// 喚醒 也就是說開始讀管道中的數(shù)據(jù)
awoken();
} else {
//... 省略
}
} else {
//... 省略
}
}
}
void Looper::awoken() {
char buffer[16];
ssize_t nRead;
do {
// 讀管道中的數(shù)據(jù)
nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
} while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
以上就是Epoll在android中的應(yīng)用。具體細(xì)節(jié),之后還會(huì)另寫一篇文章詳細(xì)說明。
要解決的問題
一個(gè)應(yīng)用進(jìn)程對(duì)應(yīng)一個(gè)Socket文件,操作系統(tǒng)中應(yīng)用無上限,因此代表有多個(gè)Socket文件接收到信息后,要知道到底發(fā)送給那個(gè)應(yīng)用進(jìn)程?所有的進(jìn)程關(guān)心的Socket都由一個(gè)線程進(jìn)行處理,所以也就是構(gòu)成I/O多路復(fù)用問題。
數(shù)據(jù)結(jié)構(gòu)
當(dāng)然這些Socket集合,當(dāng)FD=XXX發(fā)生了事件變化,那么是那些進(jìn)程需要關(guān)心的呢?
這里就涉及到Socket集合的數(shù)據(jù)結(jié)構(gòu)了。
一個(gè)Socket文件,能夠由多個(gè)進(jìn)程使用,而一個(gè)進(jìn)程也可以使用多個(gè)Socket文件,因此Socket與進(jìn)程是多對(duì)多的關(guān)系。另外,一個(gè)Socket也會(huì)有不同的事件類型。所以操作系統(tǒng)將很難判斷出將哪樣的事件給那個(gè)進(jìn)程。
鏈表/數(shù)組線性結(jié)構(gòu)
Select與Poll都是采用了線性結(jié)構(gòu)。Select允許用戶傳入三個(gè)集合,這三個(gè)集合分別對(duì)應(yīng)了Socket的三種不同的狀態(tài)的集合,讀集合、寫集合、異常集合。
Select
fd_set read_fd_set, write_fd_set, error_fd_set;
while(true) {
// 進(jìn)程讀關(guān)心、寫關(guān)心、異常時(shí)關(guān)心的Socket集合。
select(..., &read_fd_set, &write_fd_set, &error_fd_set);
for (i = 0; i < FD_SETSIZE; ++i)
if (FD_ISSET (i, &read_fd_set)){
// Socket可以讀取
} else if(FD_ISSET(i, &write_fd_set)) {
// Socket可以寫入
} else if(FD_ISSET(i, &error_fd_set)) {
// Socket發(fā)生錯(cuò)誤
}
}
因此呢,每次進(jìn)行Select操作的時(shí)候呢,會(huì)阻塞當(dāng)前的線程,在阻塞期間所有的操作系統(tǒng)產(chǎn)生的每個(gè)信息,都會(huì)通過遍歷的方式去查找這個(gè)消息是否存在于這三個(gè)集合之中。其中 FD_SETSIZE代表最大并發(fā)數(shù)量,一般是1024個(gè)并發(fā)。
Select缺點(diǎn)
- 每次需要遍歷這三種集合,并且需要進(jìn)行比較操作,浪費(fèi)了很大的時(shí)間。
- 不是一個(gè)很好的編程模型,不應(yīng)該讓用戶來設(shè)置自己的集合而是通過系統(tǒng)的Api來拿到對(duì)應(yīng)的消息。
Poll
while(true) {
events = poll(fds, ...)
for(evt in events) {
fd = evt.fd;
type = evt.revents;
if(type & POLLIN ) {
// 有數(shù)據(jù)需要讀,讀取fd中的數(shù)據(jù)
} else if(type & POLLOUT) {
// 可以寫入數(shù)據(jù)
}
else ...
}
}
Poll 在性能上呢和Select差別不大,還是需要進(jìn)行遍歷,只是說在編程模型上進(jìn)行了優(yōu)化處理。
總結(jié)Select/Poll
都是阻塞式IO,都需要進(jìn)行Socket線性集合的遍歷找到當(dāng)前進(jìn)程所需要的Socket。
EPoll
為了解決上述的問題,epoll 通過更好的方案實(shí)現(xiàn)了從操作系統(tǒng)訂閱消息。epoll 將進(jìn)程關(guān)注的文件描述符存入一棵二叉搜索樹,通常是紅黑樹的實(shí)現(xiàn)。在這棵紅黑樹當(dāng)中,Key 是 Socket 的編號(hào),值是這個(gè) Socket 關(guān)注的消息。因此,當(dāng)內(nèi)核發(fā)生了一個(gè)事件:比如 Socket 編號(hào) 1000 可以讀取。這個(gè)時(shí)候,可以馬上從紅黑樹中找到進(jìn)程是否關(guān)注這個(gè)事件。另外當(dāng)有關(guān)注的事件發(fā)生時(shí),epoll 會(huì)先放到一個(gè)隊(duì)列當(dāng)中。當(dāng)用戶調(diào)用epoll_wait時(shí)候,就會(huì)從隊(duì)列中返回一個(gè)消息。epoll 函數(shù)本身是一個(gè)構(gòu)造函數(shù),只用來創(chuàng)建紅黑樹和隊(duì)列結(jié)構(gòu)。epoll_wait調(diào)用后,如果隊(duì)列中沒有消息,也可以馬上返回。因此epoll是一個(gè)非阻塞模型。
總結(jié)一下,select/poll 是阻塞模型,epoll 是非阻塞模型。當(dāng)然,并不是說非阻塞模型性能就更好。在多數(shù)情況下,epoll 性能更好是因?yàn)閮?nèi)部有紅黑樹的實(shí)現(xiàn)。
優(yōu)點(diǎn)
- 內(nèi)部使用紅黑樹減少了內(nèi)核的比較操作;
- 對(duì)于程序員而言,非阻塞的模型更容易處理各種各樣的情況。程序員習(xí)慣了寫出每一條語句就可以馬上得到結(jié)果,這樣不容易出 Bug。


