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