從鍵盤按下一個6,到顯示出來,電腦發生了什麼?

  • 2021 年 4 月 20 日
  • 筆記

電腦領域有一個經典的問題:從你在瀏覽器中輸入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的Core處理器開始(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以後,電腦到底發生了什麼。

  1. 按下按鍵的瞬間,按鍵所在位置的開關被接通,隨後被鍵盤內部晶片檢測到,得到按鍵的掃描碼。
  2. 鍵盤控制器晶片發送一個按鍵消息,通過USB連介面傳輸到電腦主板上的USB控制器。
  3. USB控制器被集成到了主板上的PCH之中(以前的南橋晶片),一同被集成的還有負責管理所有外設中斷源的中斷控制器:IOAPIC。
  4. USB以中斷請求形式通知IOAPIC。
  5. IOAPIC根據對應中斷源的配置,生成一條中斷消息,通過系統匯流排發送出去。
  6. 這條消息之中有收件人ID,所有核的Local APIC拿到以後比對收件人是否是自己,不是自己則丟棄。
  7. Local APIC收到中斷消息後,向所在的CPU核心發起中斷
  8. CPU執行完手頭的指令後就會轉而處理中斷,先進行上下文保存,然後調取IDT表中對應中斷向量號的處理函數執行。
  9. 中斷處理函數是USB驅動程式,它將讀取鍵盤按鍵消息的掃描碼,並轉換成程式處理所需的編碼。
  10. 作業系統內核執行緒從USB驅動程式拿到輸入消息,並分發到對應程式的消息隊列。
  11. 應用程式從自己的消息隊列中獲取到鍵盤被按下的消息。

肝文不容易,現在你知道你按下6以後,電腦到底做了那些事了嗎?知道了還不趕緊雙擊666?

肝文肝的這麼努力,白嫖合適嗎?點贊在看轉發走一波啊~

往期TOP5文章

我是Redis,MySQL大哥被我害慘了!

必看!十大高性能開發技術

懂了!VMware/KVM/Docker原來是這麼回事兒

主板上來了一個新鄰居,CPU慌了!

哈希表哪家強?幾大程式語言吵起來了!