以下為gemini回答記錄。
問:Android window信息怎么傳遞到inputflinger
好的,這是一個關(guān)于 Android 系統(tǒng)底層實(shí)現(xiàn)的經(jīng)典問題。要把這個問題講清楚,我們需要理解 Android 圖形和輸入系統(tǒng)的幾個核心組件以及它們之間的協(xié)作關(guān)系。
一句話概括:WindowManagerService (WMS) 負(fù)責(zé)管理所有窗口的狀態(tài),當(dāng)窗口信息(如位置、大小、層級、可觸摸區(qū)域等)發(fā)生變化時,WMS 會將這些信息打包成一個精簡的數(shù)據(jù)結(jié)構(gòu),主動推送給 InputFlinger。
下面我們來詳細(xì)分解這個過程。
核心角色介紹
-
App Process (應(yīng)用進(jìn)程)
- 這是我們編寫的應(yīng)用程序運(yùn)行的地方。當(dāng) App 需要顯示一個界面(如 Activity、Dialog、Toast)時,它會向系統(tǒng)請求創(chuàng)建一個窗口。
-
WindowManagerService (WMS)
- 運(yùn)行在
system_server進(jìn)程中,是 Android 系統(tǒng)里所有窗口的“大管家”。 -
職責(zé):
- 管理所有窗口(Window)的生命周期、層級(Z-Order)、位置和大小。
- 負(fù)責(zé)窗口動畫。
- 與 SurfaceFlinger 協(xié)作,告訴 SurfaceFlinger 有哪些窗口(Surface)需要被合成并顯示在屏幕上。
- 關(guān)鍵職責(zé):計(jì)算每個窗口的可觸摸區(qū)域,并將這些信息傳遞給 InputFlinger。
- 運(yùn)行在
-
InputFlinger
- 一個獨(dú)立的系統(tǒng)本地(Native)進(jìn)程,是 Android 輸入系統(tǒng)的核心。
-
職責(zé):
- 從內(nèi)核的驅(qū)動節(jié)點(diǎn)(
/dev/input/event*)讀取原始的輸入事件(觸摸、按鍵、鼠標(biāo)等)。 - 處理和加工這些事件(例如,將多個觸摸點(diǎn)識別為多指手勢)。
- 關(guān)鍵職責(zé):根據(jù) WMS 提供的信息,決定這個輸入事件應(yīng)該分發(fā)給哪個窗口。
- 從內(nèi)核的驅(qū)動節(jié)點(diǎn)(
信息傳遞的詳細(xì)流程
這個過程可以分為兩個階段:窗口信息更新 和 輸入事件分發(fā)。你的問題主要關(guān)注第一個階段。
階段一:窗口信息更新 (Window Info Update)
這個流程的核心是 WMS 主動通知 InputFlinger。
-
窗口創(chuàng)建/變化
- 當(dāng)一個應(yīng)用通過
WindowManager.addView()添加一個新窗口時,這個請求通過 Binder IPC 會發(fā)送到 WMS。 - 同樣,當(dāng)一個窗口的位置、大小、可見性、或者一些重要標(biāo)志(如
FLAG_NOT_TOUCHABLE)發(fā)生改變時,WMS 也會感知到。
- 當(dāng)一個應(yīng)用通過
-
WMS 計(jì)算和整理信息
- WMS 內(nèi)部維護(hù)了一個完整的窗口列表(
WindowState對象)。當(dāng)任何窗口狀態(tài)變化時,WMS 會重新計(jì)算整個屏幕的窗口布局。 - 對于每個可見的、可交互的窗口,WMS 會計(jì)算出它的關(guān)鍵信息,包括:
- Frame (坐標(biāo)和尺寸):窗口在屏幕上的絕對位置和大小。
- Touchable Region (可觸摸區(qū)域):這是一個非常重要的信息。它不一定等于窗口的 Frame。例如,一個有圓角的窗口,它的可觸摸區(qū)域就是排除掉四個角之后的區(qū)域?;蛘叽翱诒黄渌翱诓糠终趽?,可觸摸區(qū)域就是未被遮擋的部分。
-
Flags (標(biāo)志位):比如
FLAG_NOT_TOUCHABLE(不可觸摸),FLAG_NOT_FOCUSABLE(不可獲得焦點(diǎn)),FLAG_WATCH_OUTSIDE_TOUCH(關(guān)心窗口外的觸摸事件) 等。 - Z-Order (層級):窗口的堆疊順序,決定了哪個窗口在最上面。
- Owner Info (歸屬信息):窗口所屬應(yīng)用的 UID 和 PID,用于權(quán)限檢查和事件歸屬。
- Display ID:窗口所在的顯示屏 ID。
- Input Channel:一個用于接收輸入事件的管道,稍后會講到。
- WMS 內(nèi)部維護(hù)了一個完整的窗口列表(
-
打包信息并發(fā)送
- WMS 不會把龐大的
WindowState對象直接發(fā)給 InputFlinger。它會把上述的關(guān)鍵信息提取出來,打包成一個輕量級的、專門用于輸入系統(tǒng)的數(shù)據(jù)結(jié)構(gòu),通常叫做InputWindowInfo。 - WMS 通過調(diào)用
InputManagerService(IMS) 的接口(IMS 是 InputFlinger 在 Java 層的代理),最終通過 JNI 調(diào)用到 Native 層的 InputFlinger。 - 調(diào)用的關(guān)鍵函數(shù)類似于
setInputWindows()。這個函數(shù)會把一個包含所有窗口信息的InputWindowInfo列表一次性地、原子性地更新到 InputFlinger 的InputDispatcher組件中。
- WMS 不會把龐大的
總結(jié)一下信息更新流:
App 操作 -> WMS 感知變化 -> WMS 整理窗口信息 -> WMS 創(chuàng)建 InputWindowInfo 列表 -> WMS 通過 IMS/JNI 調(diào)用 InputFlinger -> InputFlinger 更新其內(nèi)部的窗口信息列表。
階段二:輸入事件分發(fā) (Input Event Dispatch)
現(xiàn)在 InputFlinger 已經(jīng)有了最新的“窗口地圖”,當(dāng)一個觸摸事件發(fā)生時,它就能正確地找到目的地。
-
事件產(chǎn)生
- 用戶觸摸屏幕,內(nèi)核生成輸入事件。
- InputFlinger 的
InputReader線程讀取到這個原始事件。
-
尋找目標(biāo)窗口
-
InputReader將事件交給InputDispatcher線程。 -
InputDispatcher會遍歷它從 WMS 收到的InputWindowInfo列表。這個遍歷是從上到下的,即按照 Z-Order 從最頂層的窗口開始檢查。 - 對于每個窗口,
InputDispatcher會進(jìn)行判斷:- 觸摸點(diǎn)的坐標(biāo) (x, y) 是否落在該窗口的
Touchable Region內(nèi)? - 該窗口的
Flags是否允許它接收觸摸事件?(例如,沒有設(shè)置FLAG_NOT_TOUCHABLE)。
- 觸摸點(diǎn)的坐標(biāo) (x, y) 是否落在該窗口的
- 一旦找到第一個符合條件的窗口,這個窗口就是本次事件的目標(biāo)(Target)。
-
-
分發(fā)事件
- 在窗口創(chuàng)建時,WMS 已經(jīng)為它創(chuàng)建了一對
InputChannel(一個匿名管道)。一端由 InputFlinger 持有,另一端通過 Binder 傳遞給了應(yīng)用程序進(jìn)程。 -
InputDispatcher將觸摸事件通過目標(biāo)窗口對應(yīng)的InputChannel發(fā)送出去。
- 在窗口創(chuàng)建時,WMS 已經(jīng)為它創(chuàng)建了一對
-
應(yīng)用接收事件
- 應(yīng)用進(jìn)程的 UI 線程有一個 Looper 在不斷地監(jiān)聽它的
InputChannel。 - 當(dāng)讀到事件后,經(jīng)過一系列包裝,最終會調(diào)用到我們熟悉的
Activity.dispatchTouchEvent()或View.dispatchTouchEvent()方法。
- 應(yīng)用進(jìn)程的 UI 線程有一個 Looper 在不斷地監(jiān)聽它的
圖解流程
+----------------+ 1. addView() +-------------------------+
| App Process | ----------------------> | WindowManagerService |
+----------------+ (via Binder) | (in system_server) |
^ +-------------------------+
| | 2. 計(jì)算布局, 創(chuàng)建/更新
| 6. dispatchTouchEvent() | InputWindowInfo 列表
| |
+----------------+ v
| InputChannel | <--- 5. 寫入事件 --- +-------------------------+
+----------------+ | InputFlinger |
+-------------------------+
^
| 4. 尋找目標(biāo)窗口
|
+-------------------------+
| Kernel (/dev/input/*) |
+-------------------------+
^
| 3. 用戶觸摸
+---------+
| Screen |
+---------+
總結(jié)
-
誰是信息源?
WindowManagerService (WMS) 是窗口信息的唯一權(quán)威來源。 -
誰是信息消費(fèi)者?
InputFlinger 是窗口信息的消費(fèi)者,它本身不管理窗口,只使用這些信息來決策事件的分發(fā)。 -
溝通方式?
單向推送。WMS 在窗口狀態(tài)變化時,主動將精簡后的InputWindowInfo列表推送給 InputFlinger。這是一種高效的同步機(jī)制,避免了 InputFlinger 每次都需要向 WMS 查詢。 -
核心數(shù)據(jù)結(jié)構(gòu)?
InputWindowInfo,它就像一張“窗口名片”,包含了 InputFlinger 分發(fā)事件所需的所有關(guān)鍵信息。
問:android輸入事件分發(fā)是怎樣的
好的,這個問題是緊接著上一個問題的自然延伸,非常棒!我們已經(jīng)知道了 InputFlinger 是如何從 WMS 獲取“窗口地圖”的,現(xiàn)在我們來詳細(xì)看看 InputFlinger 拿到輸入事件后,是如何一步步把它送到你的 App 手里的。
整個 Android 輸入事件的分發(fā)流程可以概括為一個清晰的、自下而上的旅程:從硬件到內(nèi)核,再到系統(tǒng)原生服務(wù),最后抵達(dá)應(yīng)用程序。
核心參與者
我們再回顧一下主角們,這次我們更關(guān)注他們在事件分發(fā)中的角色:
- 硬件 (Hardware):觸摸屏、鍵盤、鼠標(biāo)等。
-
Linux 內(nèi)核 (Kernel):通過驅(qū)動程序(Drivers)將硬件信號轉(zhuǎn)化為標(biāo)準(zhǔn)的輸入事件,并暴露為設(shè)備文件(例如
/dev/input/event*)。 -
InputFlinger:Android 輸入系統(tǒng)的核心中樞,一個獨(dú)立的 Native 進(jìn)程。它包含兩個關(guān)鍵線程:
-
InputReader:負(fù)責(zé)讀取內(nèi)核的原始事件,并將其解碼、翻譯成 Android 標(biāo)準(zhǔn)的輸入事件(如
MotionEvent,KeyEvent)。 -
InputDispatcher:負(fù)責(zé)將
InputReader翻譯好的事件分發(fā)給正確的窗口。這是整個分發(fā)邏輯的核心。
-
InputReader:負(fù)責(zé)讀取內(nèi)核的原始事件,并將其解碼、翻譯成 Android 標(biāo)準(zhǔn)的輸入事件(如
-
WindowManagerService (WMS):雖然不直接參與分發(fā),但它為
InputDispatcher提供了至關(guān)重要的“窗口地圖”(InputWindowInfo列表),告訴它每個窗口的位置、層級和觸摸區(qū)域。 - 應(yīng)用程序 (App Process):輸入事件的最終消費(fèi)者。
事件分發(fā)的詳細(xì)步驟
讓我們以一次觸摸事件 (Touch Event) 為例,追蹤它的完整旅程:
第 1 步:硬件 -> 內(nèi)核 (最底層)
- 用戶觸摸屏幕。
- 觸摸屏的控制器(Controller)檢測到物理觸摸,并將其轉(zhuǎn)換為電信號。
- Linux 內(nèi)核中的觸摸屏驅(qū)動程序接收到這些信號。
- 驅(qū)動程序按照 Linux 的輸入子系統(tǒng)協(xié)議,將這些信號封裝成標(biāo)準(zhǔn)的輸入事件(包含事件類型、編碼和值),然后寫入到對應(yīng)的設(shè)備節(jié)點(diǎn)文件,比如
/dev/input/event2。
第 2 步:內(nèi)核 -> InputReader (事件的誕生)
- InputFlinger 進(jìn)程中的
InputReader線程通過EventHub組件,使用epoll機(jī)制實(shí)時監(jiān)聽所有/dev/input/*設(shè)備文件的變化。 - 當(dāng)觸摸屏的設(shè)備文件有新數(shù)據(jù)寫入時,
InputReader會被喚醒。 -
InputReader讀取這些原始的、零散的事件數(shù)據(jù)(例如,一個觸摸點(diǎn)移動可能會產(chǎn)生多個事件:X 坐標(biāo)更新、Y 坐標(biāo)更新、同步信號等)。 -
InputReader將這些原始數(shù)據(jù)進(jìn)行解碼和組裝,轉(zhuǎn)換成 Android 系統(tǒng)能夠理解的、結(jié)構(gòu)化的MotionEvent對象。這個對象包含了完整的觸摸信息:動作類型(按下、移動、抬起)、觸摸點(diǎn)坐標(biāo)、發(fā)生時間、設(shè)備 ID 等。
第 3 步:InputReader -> InputDispatcher (尋找目標(biāo))
-
InputReader將封裝好的MotionEvent對象放入一個隊(duì)列,并通知InputDispatcher線程有新事件需要處理。 -
InputDispatcher線程從隊(duì)列中取出事件,開始執(zhí)行其核心職責(zé):尋找目標(biāo)窗口 (Find Target)。 -
InputDispatcher會查詢它內(nèi)部維護(hù)的、由 WMS 同步過來的窗口列表 (InputWindowInfo list)。 - 它會從 Z-Order 最高(最頂層)的窗口開始,向下遍歷。
- 對于列表中的每一個窗口,它會進(jìn)行檢查:
- 命中測試 (Hit Test):觸摸事件的坐標(biāo) (x, y) 是否落在這個窗口的可觸摸區(qū)域 (Touchable Region) 內(nèi)?
-
權(quán)限檢查:這個窗口是否允許接收觸摸事件?(例如,窗口的
FLAG_NOT_TOUCHABLE標(biāo)志位沒有被設(shè)置)。
- 第一個通過所有檢查的窗口,就被確定為這次觸摸事件的目標(biāo)窗口 (Target Window)。一旦找到目標(biāo),遍歷就停止。
關(guān)鍵點(diǎn):這個過程是極其高效的,因?yàn)樗窃?InputFlinger 自己的內(nèi)存中完成的,不需要與 WMS 進(jìn)行任何實(shí)時通信。
第 4 步:InputDispatcher -> 應(yīng)用程序 (事件的投遞)
- 當(dāng)
InputDispatcher確定了目標(biāo)窗口后,它需要把事件發(fā)送給這個窗口所屬的應(yīng)用程序。 - 這個通信是通過一個叫做 InputChannel 的機(jī)制完成的。
InputChannel本質(zhì)上是一個基于 Unix Domain Socket 的匿名管道,它非常輕量和高效。 - 在窗口創(chuàng)建時,WMS 就已經(jīng)為它創(chuàng)建了一對
InputChannel:一端由InputFlinger持有,另一端通過 Binder 傳遞給了應(yīng)用程序進(jìn)程。 -
InputDispatcher將MotionEvent寫入目標(biāo)窗口對應(yīng)的InputChannel的服務(wù)端。 -
等待確認(rèn) (ANR 機(jī)制):在發(fā)送事件后,
InputDispatcher并不會立即處理下一個事件,而是會等待應(yīng)用程序的“回執(zhí)”。如果應(yīng)用程序在規(guī)定時間內(nèi)(通常是 5 秒)沒有處理完事件并給出響應(yīng),InputDispatcher就會認(rèn)為該應(yīng)用發(fā)生了 ANR (Application Not Responding),并向系統(tǒng)報(bào)告。
第 5 步:應(yīng)用程序內(nèi)部的分發(fā) (事件的消費(fèi))
- 在應(yīng)用程序的 UI 線程中,有一個
Looper在不斷循環(huán)。這個Looper監(jiān)聽著InputChannel的客戶端文件描述符。 - 當(dāng)
InputChannel中有數(shù)據(jù)(即觸摸事件)時,Looper會被喚醒。 -
InputEventReceiver會從InputChannel中讀取數(shù)據(jù),并在應(yīng)用的內(nèi)存中重新構(gòu)建出MotionEvent對象。 - 然后,事件被送入 View 系統(tǒng)的分發(fā)體系中,開始我們所熟知的旅程:
Activity.dispatchTouchEvent()PhoneWindow.superDispatchTouchEvent()DecorView.dispatchTouchEvent()-
ViewGroup.dispatchTouchEvent()(這里會進(jìn)行攔截onInterceptTouchEvent()和分發(fā)) -
View.dispatchTouchEvent()(這里會處理onTouchEvent())
- 當(dāng)事件處理完畢(例如
onTouchEvent返回true或false),應(yīng)用會通過InputChannel向InputDispatcher發(fā)送一個“處理完畢”的回執(zhí)信號。InputDispatcher收到后,才會繼續(xù)分發(fā)下一個事件。
圖解流程
+----------------+ 5. 事件在View體系中分發(fā) +-------------------+
| App | <---------------------------- | Activity/View |
| (UI Thread) | (dispatchTouchEvent) +-------------------+
+----------------+
^
| 4. 通過 InputChannel 接收事件
| (Looper 喚醒)
+----------------+ 3. 寫入事件到目標(biāo)Channel +-------------------+
| InputChannel | <---------------------------- | InputDispatcher |
+----------------+ | (Finds Target) |
+-------------------+
^
| 2. 遞交翻譯好的事件
|
+-------------------+
| InputReader |
| (Decodes Event) |
+-------------------+
^
| 1. 讀取原始數(shù)據(jù)
|
+----------------+ +-------------------+
| Linux Kernel | ----------------------------> | EventHub |
| (/dev/input/*) | (via epoll) +-------------------+
+----------------+
總結(jié)
- 分層與解耦:整個流程清晰地分為了內(nèi)核層、原生服務(wù)層和應(yīng)用層,各司其職。
- 單向數(shù)據(jù)流:事件從底層硬件單向流向頂層應(yīng)用。
-
效率優(yōu)先:
- 使用高效的
epoll監(jiān)聽硬件事件。 -
InputDispatcher使用預(yù)先緩存的窗口信息,避免了與 WMS 的實(shí)時 IPC。 -
InputChannel提供了低延遲的跨進(jìn)程事件傳遞通道。
- 使用高效的
- 可靠性保障:ANR 機(jī)制確保了系統(tǒng)的流暢性,能夠及時發(fā)現(xiàn)并處理無響應(yīng)的應(yīng)用。
這就是 Android 輸入事件分發(fā)的完整流程,一個設(shè)計(jì)精巧、高效且可靠的系統(tǒng)。
問:詳細(xì)講講InputFlinger吧
當(dāng)然,非常樂意為您詳細(xì)講解 InputFlinger。它是 Android 系統(tǒng)中一個極其重要但又常?!半[身”的組件。理解了它,就等于掌握了 Android 輸入系統(tǒng)的半壁江山。
一、 什么是 InputFlinger?
InputFlinger 是 Android 系統(tǒng)的一個原生 (Native) 系統(tǒng)服務(wù),它以一個獨(dú)立的進(jìn)程 (/system/bin/inputflinger) 運(yùn)行。
簡單來說,它的核心使命是:接收來自所有輸入設(shè)備(觸摸屏、鍵盤、鼠標(biāo)、手柄等)的原始信號,并將它們處理、翻譯、最終準(zhǔn)確無誤地分發(fā)給當(dāng)前應(yīng)該接收這些事件的應(yīng)用程序窗口。
可以把它想象成整個 Android 系統(tǒng)的“神經(jīng)中樞”。
二、 為什么需要 InputFlinger?
如果沒有 InputFlinger,每個需要處理輸入的應(yīng)用程序都必須自己去監(jiān)聽內(nèi)核設(shè)備、自己解碼原始信號、自己判斷事件是否屬于自己。這將導(dǎo)致巨大的混亂和低效:
- 無序競爭:所有應(yīng)用都會爭搶同一個輸入設(shè)備,無法確定誰應(yīng)該響應(yīng)。
- 缺乏仲裁:當(dāng)一個觸摸事件同時落在兩個重疊的窗口上時,誰來決定哪個窗口響應(yīng)?
- 效率低下:每個應(yīng)用都做重復(fù)的事件解碼工作,浪費(fèi) CPU 資源。
- 體驗(yàn)不一致:不同應(yīng)用對硬件的解析方式可能不同,導(dǎo)致操作體驗(yàn)千差萬別。
InputFlinger 的存在就是為了解決以上所有問題,它提供了一個集中式、有序、高效的輸入事件處理和分發(fā)模型。
三、 InputFlinger 的核心架構(gòu)與組件
InputFlinger 內(nèi)部并非鐵板一塊,它主要由兩個協(xié)同工作的關(guān)鍵線程和一個輔助模塊構(gòu)成。
(這是一個簡化的概念圖)
1. EventHub (事件集線器)
- 角色:InputFlinger 與 Linux 內(nèi)核之間的橋梁。
-
職責(zé):
-
設(shè)備發(fā)現(xiàn):通過
inotify監(jiān)視/dev/input/目錄,當(dāng)有新的輸入設(shè)備(如插入 USB 鍵盤)連接或斷開時,它能動態(tài)地識別和管理。 -
事件監(jiān)聽:使用 I/O 多路復(fù)用技術(shù)(
epoll)高效地監(jiān)聽所有已打開的輸入設(shè)備節(jié)點(diǎn) (/dev/input/event*)。當(dāng)任何一個設(shè)備產(chǎn)生數(shù)據(jù)時,EventHub會立即被喚醒。 -
原始數(shù)據(jù)讀取:從設(shè)備節(jié)點(diǎn)讀取最原始的、未經(jīng)處理的
input_event數(shù)據(jù)結(jié)構(gòu)。這些數(shù)據(jù)非常底層,例如“X 坐標(biāo)變?yōu)?200”、“Y 坐標(biāo)變?yōu)?400”、“同步信號”。
-
設(shè)備發(fā)現(xiàn):通過
2. InputReader 線程
- 角色:事件翻譯官和組裝工。
-
職責(zé):
-
獲取原始事件:不斷地從
EventHub獲取原始的input_event數(shù)據(jù)。 -
解碼與翻譯:
InputReader擁有針對不同設(shè)備類型(觸摸屏、鍵盤、手柄等)的“解碼器”(InputMapper)。它將EventHub傳來的零散的、底層的事件碼(如ABS_MT_POSITION_X)翻譯成 Android 上層能夠理解的、結(jié)構(gòu)化的事件對象。 -
事件組裝:將多個相關(guān)的原始事件組合成一個完整的、有意義的 Android 事件。例如,它會把一系列代表多點(diǎn)觸摸的原始數(shù)據(jù)組裝成一個包含所有觸摸點(diǎn)信息的
MotionEvent對象。同樣,它會把按鍵的scan code和key code轉(zhuǎn)換為KeyEvent。 - 狀態(tài)管理:跟蹤設(shè)備狀態(tài),比如鍵盤上的哪些修飾鍵(Shift, Ctrl)被按下了。
-
輸出:將組裝好的 Android 事件(如
MotionEvent,KeyEvent)放入一個隊(duì)列中,準(zhǔn)備交給InputDispatcher處理。
-
獲取原始事件:不斷地從
3. InputDispatcher 線程
- 角色:智能交通調(diào)度員,這是 InputFlinger 最核心、最智能的部分。
-
職責(zé):
-
獲取待處理事件:從
InputReader準(zhǔn)備好的隊(duì)列中取出事件。 -
尋找目標(biāo)窗口 (Find Target):這是它的核心算法。
- 它會查詢內(nèi)部維護(hù)的一份“窗口地圖”,這份地圖是由 WindowManagerService (WMS) 在窗口布局發(fā)生變化時主動推送過來的
InputWindowInfo列表。 - 它會按照窗口的 Z-Order(層級,從最上層開始)遍歷這份地圖。
- 對于觸摸事件,它進(jìn)行命中測試 (Hit Test),判斷事件坐標(biāo)是否落在窗口的可觸摸區(qū)域內(nèi)。
- 對于按鍵事件,它會尋找當(dāng)前擁有輸入焦點(diǎn) (Focused Window) 的窗口。
- 它還會檢查窗口的標(biāo)志位(如
FLAG_NOT_TOUCHABLE)來決定該窗口是否可以接收事件。
- 它會查詢內(nèi)部維護(hù)的一份“窗口地圖”,這份地圖是由 WindowManagerService (WMS) 在窗口布局發(fā)生變化時主動推送過來的
-
事件分發(fā) (Dispatch):
- 一旦找到目標(biāo)窗口,它會通過一個名為 InputChannel 的高效 IPC 管道,將事件發(fā)送給該窗口所在的應(yīng)用程序進(jìn)程。
-
InputChannel是一個基于 SocketPair 的全雙工管道,專為低延遲的事件傳遞而設(shè)計(jì)。
-
ANR 監(jiān)控 (Application Not Responding):
-
InputDispatcher在發(fā)出一個事件后,會啟動一個超時計(jì)時器(觸摸事件通常是 5 秒)。 - 如果應(yīng)用程序在規(guī)定時間內(nèi)沒有處理完事件并通過
InputChannel返回“處理完畢”的信號,InputDispatcher就會認(rèn)為該應(yīng)用已無響應(yīng)。 - 此時,它會收集相關(guān)信息,并通知 WMS,最終由 WMS 彈出我們熟悉的“應(yīng)用無響應(yīng)”對話框。
-
- 特殊事件處理:處理一些系統(tǒng)級的交互,比如“觸摸探索”(無障礙模式下)、“手勢攔截”等。
-
獲取待處理事件:從
四、 InputFlinger 的工作流程總結(jié)
讓我們把所有部分串起來,看一次完整的觸摸事件之旅:
- 硬件層:用戶觸摸屏幕。
-
內(nèi)核層:驅(qū)動將觸摸信號轉(zhuǎn)換為
input_event并寫入/dev/input/eventX。 -
EventHub:
epoll監(jiān)聽到數(shù)據(jù),讀取原始的input_event。 -
InputReader:
- 從
EventHub獲取原始事件。 - 調(diào)用
TouchInputMapper解碼器。 - 將零散的坐標(biāo)、壓力等信息組裝成一個完整的
MotionEvent對象。 - 將
MotionEvent放入分發(fā)隊(duì)列。
- 從
-
InputDispatcher:
- 從隊(duì)列中取出
MotionEvent。 - 查詢 WMS 提供的窗口列表,從上到下查找。
- 發(fā)現(xiàn)事件坐標(biāo) (x,y) 命中了窗口 A 的可觸摸區(qū)域。
- 通過窗口 A 對應(yīng)的
InputChannel將MotionEvent發(fā)送出去。 - 啟動 ANR 計(jì)時器。
- 從隊(duì)列中取出
-
應(yīng)用層:
- 應(yīng)用進(jìn)程的 UI 線程從
InputChannel讀到事件。 - 事件被封裝并傳遞給
Activity.dispatchTouchEvent(),開始在 View 樹中分發(fā)。 - 事件處理完畢后,通過
InputChannel通知InputDispatcher。
- 應(yīng)用進(jìn)程的 UI 線程從
- InputDispatcher:收到“處理完畢”的信號,取消 ANR 計(jì)時器,然后處理下一個事件。
總結(jié)
InputFlinger 是一個設(shè)計(jì)精良的系統(tǒng),它通過清晰的職責(zé)劃分(讀取、翻譯、分發(fā))和高效的機(jī)制(epoll, InputChannel, 緩存窗口信息),實(shí)現(xiàn)了以下目標(biāo):
- 解耦 (Decoupling):將硬件驅(qū)動和應(yīng)用程序徹底分開。
- 集中化管理 (Centralization):統(tǒng)一處理所有輸入,解決了沖突和仲裁問題。
- 高效 (Efficiency):使用 Native 代碼和優(yōu)化的 IPC 機(jī)制,延遲極低。
- 可靠性 (Reliability):通過 ANR 機(jī)制保證了系統(tǒng)的流暢性和響應(yīng)性。
它是 Android 能夠提供流暢、精確、可靠交互體驗(yàn)的幕后英雄。
問:請給我講講epoll吧
好的,我們來詳細(xì)聊聊 epoll。它是在高性能網(wǎng)絡(luò)編程和系統(tǒng)編程領(lǐng)域一個繞不開的話題,也是理解像 InputFlinger 這樣的系統(tǒng)組件如何高效工作的一把鑰匙。
一、 什么是 epoll?
epoll 是 Linux 內(nèi)核提供的一種 I/O 事件通知機(jī)制。它的全稱是 "event poll"。
簡單來說,它的核心功能是:讓你能夠高效地、同時監(jiān)視多個文件描述符(File Descriptor, FD),并在其中任何一個文件描述符準(zhǔn)備好進(jìn)行 I/O 操作(例如,可讀、可寫)時,能夠立即得到通知。
這里的“文件描述符”是一個非常廣泛的概念,它可以是:
- 網(wǎng)絡(luò)套接字(Socket)
- 管道(Pipe)
- 設(shè)備文件(如
/dev/input/event*) - 普通文件
二、 為什么需要 epoll?—— 從 select 和 poll 的痛點(diǎn)說起
在 epoll 出現(xiàn)之前,Linux 系統(tǒng)主要使用 select 和 poll 來實(shí)現(xiàn) I/O 多路復(fù)用。它們雖然能完成任務(wù),但在處理大量并發(fā)連接時存在嚴(yán)重的性能瓶頸。
1. select 的問題
-
文件描述符數(shù)量限制:
select使用一個fd_set位圖來存儲要監(jiān)視的文件描述符,這個位圖的大小是固定的(通常是 1024)。這意味著一個進(jìn)程最多只能同時監(jiān)視 1024 個文件描述符。對于需要處理成千上萬連接的服務(wù)器來說,這完全不夠用。 -
重復(fù)的內(nèi)存拷貝:每次調(diào)用
select時,你都需要把完整的fd_set從用戶空間拷貝到內(nèi)核空間。當(dāng)監(jiān)視的 FD 很多時,這個拷貝開銷會非常大。 -
線性掃描:
select返回后,它只會告訴你“有 FD 準(zhǔn)備好了”,但不會告訴你具體是哪一個。你必須自己遍歷整個fd_set(從 0 到 1023),逐個檢查哪個 FD 的狀態(tài)發(fā)生了變化。當(dāng)只有少數(shù)幾個 FD 活躍時,這種線性掃描是巨大的浪費(fèi)。
2. poll 的改進(jìn)與問題
poll 解決了 select 的第一個問題:
-
無數(shù)量限制:
poll使用一個pollfd結(jié)構(gòu)體數(shù)組來代替fd_set,沒有了 1024 的硬性限制。
但是,poll 仍然存在 select 的后兩個問題:
-
重復(fù)的內(nèi)存拷貝:每次調(diào)用
poll,仍然需要把整個pollfd數(shù)組從用戶空間拷貝到內(nèi)核空間。 -
線性掃描:
poll返回后,你仍然需要遍歷整個pollfd數(shù)組來找出哪些 FD 是活躍的。
總結(jié) select/poll 的核心痛點(diǎn):隨著監(jiān)視的文件描述符數(shù)量(N)的增加,每次調(diào)用的開銷(拷貝+掃描)都會呈 O(N) 級別增長,即使活躍的連接非常少。
三、 epoll 的革命性設(shè)計(jì)
epoll 的設(shè)計(jì)思想完全不同,它徹底解決了 select/poll 的痛點(diǎn)。epoll 將整個過程分成了三個步驟/API:
1. epoll_create(size)
-
作用:在內(nèi)核中創(chuàng)建一個
epoll實(shí)例,并返回一個指向該實(shí)例的文件描述符(我們稱之為epfd)。 -
核心思想:這相當(dāng)于在內(nèi)核里開辟了一塊專屬的內(nèi)存空間,用于存儲你感興趣的事件。這個空間由內(nèi)核維護(hù),只需要創(chuàng)建一次。這塊空間內(nèi)部通常包含兩個關(guān)鍵數(shù)據(jù)結(jié)構(gòu):
- 紅黑樹 (Red-Black Tree):用于高效地存儲和管理所有被監(jiān)視的文件描述符(FD)。增、刪、改、查的效率都是 O(logN)。
- 就緒列表 (Ready List):一個雙向鏈表,用于存放那些已經(jīng)準(zhǔn)備好 I/O 的文件描述符。
2. epoll_ctl(epfd, op, fd, event)
-
作用:向
epoll實(shí)例中添加 (ADD)、修改 (MOD) 或 刪除 (DEL) 你要監(jiān)視的文件描述符fd以及你關(guān)心的事件類型(如EPOLLIN表示可讀)。 -
核心思想:這是
epoll與select/poll的根本區(qū)別。你只需要告訴內(nèi)核一次“請幫我監(jiān)視這個 FD”。之后,這個信息就一直保存在內(nèi)核的紅黑樹中,不需要在每次查詢時都重復(fù)傳遞。這個操作的復(fù)雜度是 O(logN)。
3. epoll_wait(epfd, events, maxevents, timeout)
-
作用:這是主循環(huán)中唯一需要調(diào)用的阻塞函數(shù)。它會等待,直到
epoll實(shí)例中有任何一個被監(jiān)視的 FD 準(zhǔn)備好 I/O,或者超時。 -
核心思想:
-
沒有內(nèi)存拷貝:調(diào)用
epoll_wait時,你不需要傳遞任何 FD 列表。內(nèi)核已經(jīng)在epoll_ctl時知道了所有信息。 -
沒有線性掃描:當(dāng)一個 FD 準(zhǔn)備好 I/O 時,內(nèi)核會通過一個回調(diào)機(jī)制,自動將這個 FD 添加到
epoll實(shí)例的“就緒列表”中。epoll_wait所做的事情,僅僅是檢查這個“就緒列表”是否為空。如果不為空,它就把就緒列表中的 FD 直接拷貝給用戶,并返回實(shí)際就緒的 FD 數(shù)量。 -
高效返回:
epoll_wait返回時,events數(shù)組里填充的全都是已經(jīng)就緒的 FD,你不需要再自己遍歷檢查。
-
沒有內(nèi)存拷貝:調(diào)用
四、 epoll 的工作模式
epoll 還提供了兩種工作模式,可以通過 epoll_ctl 設(shè)置:
1. 水平觸發(fā) (Level Triggered, LT) - 默認(rèn)模式
-
行為:只要文件描述符的緩沖區(qū)中還有數(shù)據(jù)可讀(或可寫),
epoll_wait每次被調(diào)用都會返回這個 FD。 - 比喻:就像一個水位報(bào)警器。只要水位高于警戒線,報(bào)警器就一直響。
-
優(yōu)點(diǎn):編程更簡單,容錯性好。即使你這次沒有把緩沖區(qū)的數(shù)據(jù)讀完,下次調(diào)用
epoll_wait還會提醒你。 - 缺點(diǎn):可能會導(dǎo)致不必要的重復(fù)喚醒。
2. 邊緣觸發(fā) (Edge Triggered, ET)
-
行為:只有當(dāng)文件描述符的狀態(tài)發(fā)生變化(例如,從不可讀變?yōu)榭勺x)時,
epoll_wait才會返回這個 FD,而且只通知一次。 - 比喻:就像一個門鈴。有人按了一下,門鈴響一次。如果你不去開門,它不會再響了,直到下一個人再按。
- 優(yōu)點(diǎn):效率極高,避免了 LT 模式下的重復(fù)喚醒。非常適合高性能場景。
-
缺點(diǎn):編程更復(fù)雜。因?yàn)橹煌ㄖ淮?,你必須在收到通知后?strong>一次性地將緩沖區(qū)的數(shù)據(jù)全部讀完/寫完(通常使用非阻塞 I/O 配合循環(huán)
read/write直到返回EAGAIN),否則剩余的數(shù)據(jù)可能會“丟失”(即你再也收不到關(guān)于這些數(shù)據(jù)的通知了)。
五、 epoll 如何應(yīng)用于 InputFlinger?
現(xiàn)在我們回到 InputFlinger 的場景:
- InputFlinger 啟動時,
EventHub會調(diào)用epoll_create創(chuàng)建一個epoll實(shí)例。 - 當(dāng)
EventHub發(fā)現(xiàn)一個新的輸入設(shè)備(如/dev/input/event2),它會open這個設(shè)備文件得到一個 FD,然后調(diào)用epoll_ctl將這個 FD 以EPOLLIN(可讀) 事件添加到epoll實(shí)例中。 -
InputReader線程的主循環(huán)中,會調(diào)用epoll_wait進(jìn)行阻塞等待。 - 當(dāng)用戶觸摸屏幕時,內(nèi)核驅(qū)動向
/dev/input/event2寫入數(shù)據(jù)。 - 內(nèi)核檢測到這個 FD 變?yōu)榭勺x狀態(tài),立即將其放入
epoll實(shí)例的“就緒列表”中,并喚醒正在epoll_wait的InputReader線程。 -
epoll_wait立即返回,并且告訴InputReader:“event2這個 FD 準(zhǔn)備好了!”。 -
InputReader就直接去讀取/dev/input/event2的數(shù)據(jù),而不需要去檢查其他幾十個可能存在的設(shè)備文件(鍵盤、鼠標(biāo)等)。
這就是 epoll 的威力:即使系統(tǒng)連接了幾十個輸入設(shè)備,InputReader 也能在任何一個設(shè)備產(chǎn)生輸入的瞬間被喚醒,并且精確地知道是哪個設(shè)備,開銷與設(shè)備總數(shù)無關(guān),只與活躍設(shè)備數(shù)量有關(guān)。
總結(jié)
| 特性 |
select / poll
|
epoll |
|---|---|---|
| 數(shù)據(jù)結(jié)構(gòu) | 位圖 / 數(shù)組 (用戶空間) | 紅黑樹 + 鏈表 (內(nèi)核空間) |
| FD 傳遞 | 每次 wait 都需要完整拷貝 |
只需通過 ctl 設(shè)置一次 |
| 工作方式 | 輪詢 (Polling) | 回調(diào) (Callback-based) |
| 返回結(jié)果 | 返回后需遍歷整個列表找活躍 FD | 直接返回活躍 FD 列表 |
| 性能 | O(N),N 為監(jiān)視的總 FD 數(shù) | O(k),k 為活躍的 FD 數(shù) |
| 模式 | 僅水平觸發(fā) | 支持水平觸發(fā) (LT) 和邊緣觸發(fā) (ET) |
epoll 是構(gòu)建高性能、高并發(fā)服務(wù)器和系統(tǒng)服務(wù)的基石,它的設(shè)計(jì)思想——將一次性設(shè)置和重復(fù)性等待分離,利用內(nèi)核回調(diào)避免無用輪詢——是理解現(xiàn)代 I/O 模型的關(guān)鍵。
問:給我講講 I/O 模型吧
回答:好的,非常樂意為您講解 I/O 模型。這是一個非?;A(chǔ)且核心的計(jì)算機(jī)科學(xué)概念,尤其在網(wǎng)絡(luò)編程和系統(tǒng)編程中至關(guān)重要。理解不同的 I/O 模型,能幫助我們明白為什么有些程序快、有些慢,以及如何設(shè)計(jì)高性能的系統(tǒng)。
一、 什么是 I/O?
首先,我們來定義一下 I/O(Input/Output)。在計(jì)算機(jī)中,I/O 操作指的是數(shù)據(jù)在不同存儲介質(zhì)之間的流動過程。最典型的就是數(shù)據(jù)在內(nèi)核空間 (Kernel Space) 和 用戶空間 (User Space) 之間的傳輸。
- 用戶空間:應(yīng)用程序運(yùn)行的地方,我們寫的代碼大部分都在這里。
- 內(nèi)核空間:操作系統(tǒng)內(nèi)核運(yùn)行的地方,它直接管理硬件(如網(wǎng)卡、硬盤)。
一個典型的網(wǎng)絡(luò) I/O 讀取操作(如 read 系統(tǒng)調(diào)用)通常包含兩個階段:
-
等待數(shù)據(jù)準(zhǔn)備 (Waiting for data to be ready):
- 內(nèi)核等待網(wǎng)絡(luò)上的數(shù)據(jù)包到達(dá)。
- 數(shù)據(jù)到達(dá)后,內(nèi)核將其從網(wǎng)卡緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)。
- 這個階段數(shù)據(jù)還在內(nèi)核空間。
-
數(shù)據(jù)從內(nèi)核拷貝到用戶空間 (Copying data from kernel to user space):
- 內(nèi)核將數(shù)據(jù)從其內(nèi)部緩沖區(qū)拷貝到我們應(yīng)用程序指定的緩沖區(qū)(比如
read函數(shù)傳入的 buffer)。 - 這個階段完成后,
read函數(shù)才返回,我們的應(yīng)用程序才能處理這些數(shù)據(jù)。
- 內(nèi)核將數(shù)據(jù)從其內(nèi)部緩沖區(qū)拷貝到我們應(yīng)用程序指定的緩沖區(qū)(比如
I/O 模型的核心區(qū)別,就在于應(yīng)用程序如何處理這兩個階段,特別是第一個階段的“等待”。
二、 五種經(jīng)典的 I/O 模型
根據(jù) UNIX 網(wǎng)絡(luò)的定義,主要有五種 I/O 模型。我們從最簡單到最復(fù)雜來逐一講解。
1. 阻塞 I/O (Blocking I/O - BIO)
這是最簡單、最常見、也是默認(rèn)的 I/O 模型。
-
流程:
- 應(yīng)用程序調(diào)用一個 I/O 函數(shù)(如
recvfrom)。 - 如果內(nèi)核的數(shù)據(jù)還沒準(zhǔn)備好(階段一),那么應(yīng)用程序的整個線程將被內(nèi)核掛起(阻塞),無法做任何其他事情。
- 直到數(shù)據(jù)準(zhǔn)備好,并且從內(nèi)核拷貝到用戶空間(階段二)完成,
recvfrom函數(shù)才會返回。 - 應(yīng)用程序線程解除阻塞,繼續(xù)執(zhí)行。
- 應(yīng)用程序調(diào)用一個 I/O 函數(shù)(如
比喻:你去餐廳吃飯,點(diǎn)完餐后,你就坐在座位上什么也不干,一直等到服務(wù)員把菜端到你面前,你才開始吃飯。在等菜的整個過程中,你被“阻塞”了。
優(yōu)點(diǎn):編程模型最簡單,邏輯清晰。
缺點(diǎn):性能極差。一個線程在任意時刻只能處理一個連接的 I/O。如果想同時處理多個連接,就需要為每個連接創(chuàng)建一個線程,這會導(dǎo)致大量的線程創(chuàng)建開銷和上下文切換開銷,嚴(yán)重限制了并發(fā)能力。
2. 非阻塞 I/O (Non-blocking I/O - NIO)
為了解決阻塞 I/O 的問題,非阻塞 I/O 出現(xiàn)了。
-
流程:
- 應(yīng)用程序?qū)?I/O 設(shè)備設(shè)置為非阻塞模式。
- 應(yīng)用程序調(diào)用
recvfrom。 - 如果內(nèi)核數(shù)據(jù)還沒準(zhǔn)備好,
recvfrom會立即返回一個錯誤碼 (如EWOULDBLOCK或EAGAIN),而不是阻塞線程。 - 應(yīng)用程序可以去做別的事情。
- 但應(yīng)用程序不知道數(shù)據(jù)什么時候準(zhǔn)備好,所以它必須反復(fù)地、主動地去調(diào)用
recvfrom詢問內(nèi)核:“數(shù)據(jù)好了嗎?”、“數(shù)據(jù)好了嗎?”... 這種行為被稱為輪詢 (Polling)。 - 當(dāng)某次輪詢發(fā)現(xiàn)數(shù)據(jù)準(zhǔn)備好了,
recvfrom就不會返回錯誤了。此時,它會阻塞住,等待數(shù)據(jù)從內(nèi)核拷貝到用戶空間(階段二),然后返回成功。
比喻:你去餐廳吃飯,點(diǎn)完餐后,你每隔一分鐘就跑去廚房問廚師:“我的菜好了嗎?”。在問的間隙,你可以玩手機(jī)(做別的事),但你需要不停地去問,非常耗費(fèi)精力。
優(yōu)點(diǎn):線程不會被 I/O 等待阻塞,可以在等待期間執(zhí)行其他任務(wù)。
缺點(diǎn):輪詢會大量消耗 CPU 時間。即使大部分時間數(shù)據(jù)都沒準(zhǔn)備好,你的程序也在不停地做無用的系統(tǒng)調(diào)用,造成 CPU 資源浪費(fèi)。
3. I/O 多路復(fù)用 (I/O Multiplexing)
這是實(shí)踐中用得最多、最重要的一種模型,也是 select, poll, epoll 的用武之地。它旨在解決非阻塞 I/O 中“盲目輪詢”的問題。
-
流程:
- 應(yīng)用程序先調(diào)用一個特殊的函數(shù)(如
select,poll,epoll_wait),并將一批感興趣的文件描述符(FDs)傳遞給它。 - 這個調(diào)用是阻塞的,但它不是阻塞在某個具體的 I/O 操作上,而是阻塞在“等待多個 FD 中任意一個就緒”這個事件上。
- 內(nèi)核會同時監(jiān)視這一批 FD。當(dāng)其中任何一個或多個 FD 的數(shù)據(jù)準(zhǔn)備好了(階段一完成),
select函數(shù)就會返回,并告訴應(yīng)用程序哪些 FD 已經(jīng)就緒。 - 然后,應(yīng)用程序再針對那些已經(jīng)確定就緒的 FD,調(diào)用
recvfrom進(jìn)行數(shù)據(jù)讀取。 - 因?yàn)閿?shù)據(jù)已經(jīng)準(zhǔn)備好了,所以這次
recvfrom調(diào)用通常不會阻塞在階段一,而是直接進(jìn)入階段二(數(shù)據(jù)拷貝),然后很快返回。
- 應(yīng)用程序先調(diào)用一個特殊的函數(shù)(如
比喻:你去餐廳吃飯,但你不是一個人,而是和一大群朋友一起。你們點(diǎn)完餐后,派一個代表(
select)守在出菜口。這個代表就一直等著,什么也不干(阻塞)。當(dāng)任何一道菜(不分是誰的)做好時,出菜口的服務(wù)員會通知這個代表。然后代表再告訴對應(yīng)的朋友:“你的菜好了,快去拿!”。優(yōu)點(diǎn):可以用一個線程處理大量的并發(fā)連接。線程只在
select/epoll上阻塞一次,而不是在每個連接上都阻塞。大大提高了系統(tǒng)的并發(fā)能力。缺點(diǎn):編程模型比阻塞 I/O 復(fù)雜。以
select為例,性能仍有瓶頸(我們在上一個回答中已詳細(xì)討論)。epoll則是對這一模型的終極優(yōu)化。
4. 信號驅(qū)動 I/O (Signal-driven I/O)
這是一種不常用的模型,了解即可。
-
流程:
- 應(yīng)用程序開啟套接字的信號驅(qū)動功能,并通過
sigaction系統(tǒng)調(diào)用安裝一個信號處理函數(shù)。 - 應(yīng)用程序的進(jìn)程可以去做其他事情,不會被阻塞。
- 當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備好時(階段一完成),內(nèi)核會為該進(jìn)程生成一個
SIGIO信號。 - 應(yīng)用程序在之前安裝的信號處理函數(shù)中,調(diào)用
recvfrom來讀取數(shù)據(jù)。
- 應(yīng)用程序開啟套接字的信號驅(qū)動功能,并通過
比喻:你去餐廳吃飯,點(diǎn)完餐后,你給前臺留了你的手機(jī)號,然后你就去逛街了。當(dāng)你的菜做好時,服務(wù)員會給你打電話(發(fā)信號)。你接到電話后,再回到餐廳去取菜。
優(yōu)點(diǎn):線程在等待數(shù)據(jù)期間不會被阻塞。
缺點(diǎn):在信號量很大的情況下,信號處理的開銷會很大;而且它只適用于 UDP,對于 TCP 這種流式協(xié)議,信號的觸發(fā)時機(jī)和數(shù)據(jù)量不好控制。
5. 異步 I/O (Asynchronous I/O - AIO)
這是最理想化的 I/O 模型,也是“真·異步”。
-
流程:
- 應(yīng)用程序調(diào)用一個異步 I/O 函數(shù)(如
aio_read),并把緩沖區(qū)地址、大小、文件描述符以及一個完成后的通知方式(如信號或回調(diào)函數(shù))都告訴內(nèi)核。 - 這個調(diào)用會立即返回,應(yīng)用程序的線程完全不被阻塞,可以去做任何其他事情。
- 內(nèi)核會獨(dú)立完成所有工作:它會自己等待數(shù)據(jù)準(zhǔn)備好(階段一),然后自己將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序指定的緩沖區(qū)(階段二)。
- 當(dāng)兩個階段都全部完成后,內(nèi)核才會通過之前指定的方式(如調(diào)用回調(diào)函數(shù))來通知應(yīng)用程序:“你的數(shù)據(jù)已經(jīng)準(zhǔn)備好了,并且已經(jīng)放在你指定的 buffer 里了,你可以直接用了”。
- 應(yīng)用程序調(diào)用一個異步 I/O 函數(shù)(如
比喻:你去餐廳吃飯,點(diǎn)完餐后,你告訴服務(wù)員:“菜好了之后,請直接送到我家(指定緩沖區(qū)),送到后給我發(fā)個短信(回調(diào)通知)就行了?!?然后你就直接回家了。你不需要等待,也不需要自己去取,整個過程由餐廳(內(nèi)核)全權(quán)代勞。
優(yōu)點(diǎn):實(shí)現(xiàn)了真正的非阻塞,應(yīng)用程序在 I/O 的兩個階段都無需關(guān)心,并發(fā)性能理論上是最高的。
缺點(diǎn):編程模型最復(fù)雜。在 Linux 下,原生的 AIO(
libaio)支持并不完善,通常只對本地文件 I/O 有較好支持,對網(wǎng)絡(luò) I/O 的支持不佳。因此,在實(shí)踐中,很多所謂的“異步框架”(如 Node.js, Netty)其實(shí)是基于 I/O 多路復(fù)用(epoll)在應(yīng)用層模擬出的異步效果。
總結(jié)與對比
| I/O 模型 | 等待數(shù)據(jù)階段 (Phase 1) | 拷貝數(shù)據(jù)階段 (Phase 2) | 并發(fā)能力 |
|---|---|---|---|
| 阻塞 I/O | 阻塞 | 阻塞 | 差 (1線程:1連接) |
| 非阻塞 I/O | 非阻塞 (需輪詢) | 阻塞 | 中 (受CPU輪詢限制) |
| I/O 多路復(fù)用 | 阻塞在 select/epoll 上 |
阻塞 | 好 (1線程:N連接) |
| 信號驅(qū)動 I/O | 非阻塞 | 阻塞 | 中 |
| 異步 I/O (AIO) | 非阻塞 | 非阻塞 | 極好 (真異步) |
關(guān)鍵區(qū)分:同步 I/O vs 異步 I/O
- 同步 I/O:指應(yīng)用程序必須自己參與到 I/O 操作中,并等待或主動查詢 I/O 操作的完成。前四種模型(阻塞、非阻塞、多路復(fù)用、信號驅(qū)動)都屬于同步 I/O。因?yàn)樵跀?shù)據(jù)從內(nèi)核拷貝到用戶空間的階段(階段二),應(yīng)用程序的線程都是被阻塞的。
- 異步 I/O:指應(yīng)用程序發(fā)起 I/O 請求后就可以立即返回做其他事,由內(nèi)核完成所有 I/O 操作后,再通知應(yīng)用程序。只有 AIO 是真正的異步 I/O。
在現(xiàn)代高性能服務(wù)器開發(fā)中,I/O 多路復(fù)用(特別是 epoll)是絕對的主流和基石。