計算機領域有一個經典的問題:從你在瀏覽器中輸入URL並按下回車,到網頁渲染出來,這中間發生了什么?
通過這個問題,可以考察候選人對計算機網絡的理解程度,因此出現在數不清的面試場合。
毋庸置疑,這是一個好問題,我也看到不下100篇文章在探討這個問題的答案。
而今天,我想跟大家探討的是另外一個問題:從你在鍵盤上按下一個“6”,到屏幕上顯示出來,計算機發生了什么?
這個問題無論從空間尺度還是時間尺度比起開始那個問題都更小得多。
空間尺度上,這個問題探討的范圍只限於一台計算機上,沒有跨越網絡。
時間尺度上,第一個問題的時間尺度在秒級別,而這個問題的時間尺度在毫秒級別。
尺度雖然小了但背后的技術知識並不少。
我相信,等你看完這篇文章,搞清楚這個問題的答案,你將對計算機組成原理、操作系統、CPU這些東西有完全不一樣的理解。
准備好,咱們出發!
0x01: 按下按鍵,鍵盤做了什么
早期的計算機,大部分都是PS2的接口,就是這玩意:
但這種接口插起來不方便,也不通用,近些年USB接口鍵盤越來越多了,所以咱們就以USB鍵盤為研究對象。
當你按下鍵盤按鍵的瞬間,這個按鍵位置下的電路“開關”將會被接通,而這樣的開關每一個按鍵下面都有,它們共同組成了一個矩陣:
全局矩陣就是這個樣子的:
如果你拆開鍵盤看過,你會發現在鍵盤的內部有類似下面這樣的一個芯片,它負責周期性的掃描電路,檢測哪些位置的按鍵被按下。
當它檢測到按鍵按下事件,將拿到對應鍵位的鍵盤掃描碼(注意按下和彈起對應不同的掃描碼),然后通過USB接口的通信協議,封裝一個按鍵消息傳遞出去。在這個消息中,包含了你按下/彈起鍵位的掃描碼,如果有多個按鍵,消息中就會有多個掃描碼。
鍵盤USB連接頭連接到了計算機主板上的USB接口,USB接口背后是主板上的USB總線系統,於是這個按鍵消息順着鍵盤的連線,穿過USB接口來到了USB總線上。
而USB總線上,連接了USB控制器芯片,是它在與USB設備進行“通話”。
0x02: 高級可編程中斷控制器APIC
USB控制器拿到了按鍵消息后,並不能直接提交給CPU,還要通過另外一個管事兒的投遞這個消息,這個管事兒的就是中斷控制器。
提到中斷控制器,你可能在很多地方看到過一個叫8259A的芯片:
然后會告訴你鍵盤通過IRQ1的中斷輸入源連接進去:
但現在請忘記它,這玩意已經是上個世紀作古的產物,我保證你拆開你的計算機,一定找不到它。
究其原因,還是因為CPU多核技術的興起,8259A這個東西早已滿足不了時代的需要,換了另外一個更高級的中斷控制器,APIC。
沒錯,它的名字就是這么簡單直接:高級可編程中斷控制器。
這個更高級的管事兒的到底哪里高級呢?
首先,它不是一塊芯片,而是分了兩部分:Local APIC和I/O APIC。
Local APIC像是外包團隊一樣,入駐到了CPU的每個核心,負責中斷每個核。
I/O APIC則獨立在CPU外面,接收所有I/O設備的中斷源。
來看一個早期的IOAPIC芯片:82093AA
就是它代替了傳統的8259A的PIC來總管主板上這些外設的中斷信號,這家伙的管腳圖長這樣:
你可以數一下,負責中斷源的輸入引腳有INTIN0-INTIN23,總共24個,比傳統的兩塊8259A的芯片級聯起來的數量還要多。
如果你拆開你的電腦主板,我保證你依然看不到這個叫IOAPIC的芯片。因為這個家伙現在已經被集成到了南橋之中了。
啥?南橋是啥?接下來需要補充一點計算機主板的知識了。
0x03: 計算機主板結構
在傳統計算機主板上,分為了CPU+北橋+南橋的經典架構:
北橋和南橋是主板上除CPU外最重要的2個芯片,所謂南北,是因為在畫圖位置上,上北下南,因而得名。
北橋聯通着CPU,負責連接內存、顯卡等高速設備。
南橋聯通着北橋,負責連接網卡、硬盤、鍵盤、鼠標這些低速設備。
你可以這樣理解:CPU是整個主板上的大明星,主板上其他所有設備都要圍繞它來轉,這明星有兩個經紀人,一個負責對接速度快的,一個負責對接速度慢的。
從Intel的酷睿處理器開始(2008年),將北橋芯片的功能集成到了CPU之中,從此主板上就只剩一個南橋了,於是也沒有南北之分了,甚至改頭換面,換了個名字:PCH。
這個叫PCH的家伙可不簡單,它現在要對接CPU,還要對接PCI總線、ISA總線上的一堆設備。
我們的鍵盤連接到的是USB總線,也是對接到這個PCH芯片。
通過cpu-z工具,可以看到自己電腦主板上的PCH芯片型號:
如上圖所示,我的這台電腦是B360芯片,你可以在Intel的官網查詢到它的詳細資料。
那這玩意兒在電腦主板哪個位置呢:
拿掉上面的散熱片,這家伙長這樣,其貌不揚:
在這個小小的芯片里,就集成有負責跟USB設備進行通信的USB控制器,還有前面說的負責中斷CPU的高級可編程中斷控制器IOAPIC,這兩個家伙在今天討論的問題中扮演了關鍵角色。
USB控制器負責與USB設備通信,它將拿到USB鍵盤傳輸過來的那個按鍵消息包。
0x04: 中斷信號的投遞
現在USB控制器和APIC已經都集成到了PCH中,內部的結構不得而知,但總體來說,USB控制器拿到按鍵消息后,然后通過IOAPIC的中斷源輸入管腳發起通知:老哥,我這有情況,快幫我通知CPU老大。
在IOAPIC的內部,有一個表格PRT,記錄了中斷分發的配置信息,24個中斷源就有24個表項(其實還有一部分保留的)。表格中的每一項叫RTE,每項占據64bit。
來自USB控制器的電信號輸入到IOAPIC之后,IOAPIC會根據事先編程配置的信息,通過對應的表項RTE格式化出一條中斷消息,然后通過總線系統發出去。
在早期,IOAPIC和CPU內部的Local APIC之間有專屬的APIC總線來聯系,但從奔騰4開始就取消了,使用公共的總線系統來傳遞中斷消息。
消息發出去后,誰來接收呢?
在這個中斷消息中,填寫有收件人:Local APIC的標識號。
總線系統上的信號通過CPU的針腳傳輸到了CPU內部,內部所有核的Local APIC都能收到這個中斷消息,但只有一個核的Local APIC檢測后發現收件人是自己,其他人都會忽略這條消息。
發現收件人是自己的那個Local APIC,開始通知自己所在的這個核有中斷請求來了。
CPU的核心一直在不停的執行指令,在每個指令周期的最后,都會去檢查一下是不是有中斷請求過來,在執行完手頭這條指令后,它發現了Local APIC提交的中斷請求。
接下來,就是CPU開始來處理這個中斷消息的時候了。
0x05: 中斷處理
第一個動作,保存執行上下文。
所謂中斷,從字面來講就是中途打斷的意思,就好比你正在寫着代碼,突然有產品來找你增加需求,你被打斷了。人倒還好,咱們有記憶能力,跟產品溝通完成后,還能回去接着原來的地方繼續寫代碼。但機器沒有記憶思維,在打斷去干別的事情之前,必須把原來做的事情保存起來,這樣一會兒才能回來繼續做剩下的事。
這個保存的過程,就叫執行上下文保存。那保存在哪里呢?
答案就是線程的棧。
但是要注意,這里的棧,不是咱們平時看到的那個線程棧,而是另外一個位於內核地址空間的棧。
不管是Windows還是Linux,基本上每個線程在執行的時候都有兩個棧,一個用於我們編寫的應用程序在用戶態模式下執行代碼時使用,叫用戶棧,另一個用於程序因為系統調用、異常、中斷等情況進入內核模式下執行的時候使用,叫內核棧,相比用戶棧,內核棧的空間要小得多。
注意:也不是每個線程都有兩個棧,有一些操作系統的純內核線程就只有內核棧,沒有用戶棧。
發生中斷時,CPU將自動將當前執行的上下文保存在內核棧的頂部,所謂上下文,其實就是一堆寄存器的值。注意這個動作不是操作系統軟件完成的,而是CPU內部的硬件電路自動完成。
第二個動作,執行中斷處理函數
保存完上下文,接着就要去處理中斷了。怎么處理,那就是操作系統的工作了。
CPU的每一個核,都有一個中斷描述符表IDT,位於內存之中,這個表有256項,每一個表項都記錄了一個處理函數的地址。每個核的內部還有一個叫IDTR的寄存器,指向了這個表。
要注意,IDT雖然是叫做中斷描述符表,但里面的256項內容卻不全是用來記錄中斷處理函數的,還有異常、陷阱(軟中斷)、任務這些。
表格中的處理函數地址,是操作系統在啟動之初就安排好了,這其中就有我們的鍵盤中斷處理函數。
當中斷發生時,CPU將根據中斷向量號,從IDTR寄存器指向的表格中,取出索引是向量號的那一個表項,跳轉到里面記錄的函數地址,開始執行代碼,這個過程依然是CPU的硬件電路完成的。
那這個中斷向量號從哪兒來的呢?
答案是在IOAPIC發來的那條消息中,除了收件人Local APIC的標識,還有處理中斷所需要的中斷向量號。
再往前追溯,這個中斷向量號其實是配置在前面說的IOAPIC內部的那個叫PRT的表格中的,操作系統啟動之初一項重要的工作就是對APIC進行編程(所謂編程其實就是寫他們內部的這些配置表,也叫寄存器),設定好每一個中斷源對應的中斷向量號是多少,這樣24個中斷源與對應的中斷向量號之間的映射關系就被確立起來了。
除了給中斷源分配向量號,操作系統還有一項工作就是指定哪些核來處理哪些中斷。我之前寫過一篇趣文故事就是講的這部分知識:CPU明明8個核,網卡為啥拼命折騰一號核?
接下來就是操作系統(准確來說是操作系統中的設備驅動程序)開始來處理這個中斷消息了。
具體的驅動處理部分就不詳述了,不同版本的系統處理略有不同,在微軟的官網上,可以找到這么一張圖,針對USB輸入設備(鍵盤、鼠標)的驅動處理棧結構圖:
總體來說,Windows操作系統介入中斷處理后,經過一系列驅動程序(USB、HID等)的處理后,進行掃描碼的轉換,然后把按鍵的消息最終投遞到了一個叫Win32k.sys的家伙那里。
0x06: 操作系統介入
讓我們把視線從硬件部分轉移到操作系統上來。Windows是一個基於視窗的圖形化的操作系統,絕大部分程序都是基於消息驅動。這一點,做過Windows客戶端開發的朋友應該不會陌生。
Windows上有圖形窗口的程序形態各異,功能千差萬別,但它們都有一個共同之處:基於消息驅動。
這些消息可能來自於鍵盤、鼠標、其他進程甚至網絡,一個典型的Windows程序,其主線程一定有一個下面的消息循環:
while(GetMessage()) {
TranslateMessage();
DispatchMessage();
}
主線程不斷調用GetMessage() 獲取消息,然后分發處理,如果沒有消息,GetMessage將會阻塞。
這個GetMessage()是從哪里獲取消息呢?
答案是消息隊列。
每一個具有圖形可視化窗口的程序都有一個消息隊列,維護在內核空間,GetMessage()就是從這里源源不斷的取出消息來處理。你的每一次鍵盤按鍵,每一次鼠標點擊,每一次鼠標移動,都會產生消息被投放到這個隊列中,等待取出處理。
那么問題又來了,你在鍵盤按下后產生的消息,是被誰投遞到了這里呢?還有,每一個窗口程序都有消息隊列,那我按下的鍵盤消息,到底該被投遞給誰呢?
答案正是在前面說的那個叫Win32k.sys的家伙之中!這是Windows內核實現圖形用戶界面一個重要的模塊,里面有一個內核線程在專門負責干這事——不斷從鍵盤驅動獲取按鍵事件,然后封裝成消息,再結合當前桌面激活的窗口,定位到對應的消息隊列,把這個消息給投遞過去。
於是,應用程序的消息循環中,GetMessage()函數將會拿到一個代表鍵盤按鍵被按下的WM_KEYDOWN消息。
再回過頭去看下那個消息循環,拿到消息后會有一個“轉換動作”:TranslateMessage()。這個函數將對按鍵消息進行一次翻譯,翻譯成一個WM_CHAR消息,表示有字符輸入消息來了,這個消息的一個字段會標識輸入的是6這個字符。
最終,應用程序終於收到了一個參數是6的WM_CHAR消息,知道用戶按了一個6,接下來就是在顯示器上把它給顯示出來了。
總結
文章有點長,現在來總結梳理下,按下鍵盤上的6以后,計算機到底發生了什么。
- 按下按鍵的瞬間,按鍵所在位置的開關被接通,隨后被鍵盤內部芯片檢測到,得到按鍵的掃描碼。
- 鍵盤控制器芯片發送一個按鍵消息,通過USB連接口傳輸到計算機主板上的USB控制器。
- USB控制器被集成到了主板上的PCH之中(以前的南橋芯片),一同被集成的還有負責管理所有外設中斷源的中斷控制器:IOAPIC。
- USB以中斷請求形式通知IOAPIC。
- IOAPIC根據對應中斷源的配置,生成一條中斷消息,通過系統總線發送出去。
- 這條消息之中有收件人ID,所有核的Local APIC拿到以后比對收件人是否是自己,不是自己則丟棄。
- Local APIC收到中斷消息后,向所在的CPU核心發起中斷
- CPU執行完手頭的指令后就會轉而處理中斷,先進行上下文保存,然后調取IDT表中對應中斷向量號的處理函數執行。
- 中斷處理函數是USB驅動程序,它將讀取鍵盤按鍵消息的掃描碼,並轉換成程序處理所需的編碼。
- 操作系統內核線程從USB驅動程序拿到輸入消息,並分發到對應程序的消息隊列。
- 應用程序從自己的消息隊列中獲取到鍵盤被按下的消息。
往期TOP5文章