0x01 問題演示
下面是用rust寫的一段測試程序,邏輯非常簡單,就是讀取用戶輸入,然后將其輸出。
運(yùn)行這個程序,然后按Ctrl-C:
由上圖可見,該程序沒有收到任何輸入,當(dāng)然也沒有任何輸出,這個程序就退出了。
為什么Ctrl-C會導(dǎo)致當(dāng)前運(yùn)行程序退出呢?
0x02 程序退出原因
上面的測試程序之所以會退出,是因?yàn)镃trl-C會告訴linux內(nèi)核,讓其發(fā)送SIGINT信號給當(dāng)前運(yùn)行程序,該信號的默認(rèn)行為是殺掉目標(biāo)進(jìn)程,所以就有了上面的現(xiàn)象。
但是,SIGINT信號,以及其他的各種信號,都是可以捕獲的,這樣我們就可以修改信號的默認(rèn)行為,比如將SIGINT信號的默認(rèn)行為,修改成輸出一些日志,而不是殺掉當(dāng)前進(jìn)程。
將上面的測試代碼改成下面的樣子:
上圖中我們捕獲了SIGINT信號,并且在收到該信號后,輸出 got SIGINT。
運(yùn)行看下:
這次再按Ctrl-C,程序就不會退出了,而且還會輸出 got SIGINT。
由上可見,Ctrl-C導(dǎo)致程序退出,確實(shí)是因?yàn)樵摪存I使內(nèi)核發(fā)送了SIGINT信號到目標(biāo)進(jìn)程,進(jìn)而導(dǎo)致目標(biāo)進(jìn)程被殺死。
那為什么Ctrl-C會觸發(fā)SIGINT信號呢?
在回答這個問題之前,我們要先了解下terminal emulator,即終端模擬器,的運(yùn)行機(jī)制。
0x03 Terminal Emulator的運(yùn)行機(jī)制
我當(dāng)前使用的terminal emulator為 alacritty,后面如果涉及到terminal emulator的源碼分析,就是基于這個項(xiàng)目。
當(dāng)然,以下講的terminal emulator的運(yùn)行機(jī)制,對于其他terminal emulator也同樣適用。
當(dāng)我們在圖形化界面,打開一個terminal emulator時,terminal emulator會調(diào)用openpty函數(shù),向linux內(nèi)核申請一個pty數(shù)據(jù)通道,當(dāng)該pty數(shù)據(jù)通道創(chuàng)建成功后,linux內(nèi)核會返回兩個文件描述符,即兩個fd,給terminal emulator,這兩個fd,就代表了新創(chuàng)建pty數(shù)據(jù)通道的兩端,分別為master端和slave端。
當(dāng)向master端的fd寫數(shù)據(jù)時,該數(shù)據(jù)就可以從slave端的fd讀出來,當(dāng)向slave端的fd寫數(shù)據(jù)時,該數(shù)據(jù)就能從master端的fd讀出來。
當(dāng)pty數(shù)據(jù)通道創(chuàng)建完畢后,terminal emulator就會調(diào)用fork函數(shù),啟動一個子進(jìn)程,該子進(jìn)程用來運(yùn)行shell程序,比如bash、zsh等,同時會將該shell的標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出,都設(shè)置為上面通過openpty函數(shù)獲取的slave端的fd。
shell在啟動成功后,會一直等待著從標(biāo)準(zhǔn)輸入,即slave fd,里接收要執(zhí)行的命令。
當(dāng)terminal emulator啟動成功后,我們就可以在其內(nèi)部輸入命令了,我們輸入的命令,會被terminal emulator寫入到master fd里,這樣在shell子進(jìn)程中,就可以通過標(biāo)準(zhǔn)輸入,即slave fd,接收到這個命令,并開始執(zhí)行。
shell在執(zhí)行接收到的命令時,也是通過fork函數(shù),創(chuàng)建一個子進(jìn)程,然后在子進(jìn)程里執(zhí)行該命令對應(yīng)的程序。
不過,這里需要注意的是,子進(jìn)程中運(yùn)行的命令程序,其標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤輸出都是繼承自shell,即它的標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤輸出的值都是slave fd。
這樣,當(dāng)命令程序?qū)懭罩镜綐?biāo)準(zhǔn)輸出時,其實(shí)際上是寫到了slave fd里,如此,在terminal emulator里,就可以通過master fd,讀取到這些日志信息,并在terminal emulator里顯示出來。
同樣的道理,當(dāng)我們此時在terminal emulator里輸入內(nèi)容時,該內(nèi)容會被terminal emulator寫入到master fd,進(jìn)而就可以被命令程序進(jìn)程,從標(biāo)準(zhǔn)輸入,即slave fd,里讀出來。
這里大家可能會有個疑問,即shell進(jìn)程和當(dāng)前運(yùn)行的命令程序進(jìn)程,他們的標(biāo)準(zhǔn)輸入都是slave fd,那為什么我們寫入到terminal emulator里的內(nèi)容,是被命令程序進(jìn)程讀出來,而不是被shell進(jìn)程讀出來呢?
這個就涉及到terminal emulator使用權(quán)的概念了。
當(dāng)shell進(jìn)程剛啟動成功后,terminal emulator的使用權(quán)自然是shell的,此時我們在terminal emulator里輸入的內(nèi)容,會被shell從標(biāo)準(zhǔn)輸入,即slave fd,里讀出來。
當(dāng)shell啟動一個子進(jìn)程,并用該進(jìn)程運(yùn)行命令程序時,它會把terminal emulator的使用權(quán),轉(zhuǎn)交給該命令程序進(jìn)程,此時我們在terminal emulator里輸入的內(nèi)容,會被該命令程序進(jìn)程從它的標(biāo)準(zhǔn)輸入,即slave fd,里讀出來的。
當(dāng)該命令程序進(jìn)程退出后,linux內(nèi)核會通知shell進(jìn)程,告知它啟動的子命令進(jìn)程已經(jīng)結(jié)束了,此時shell會把terminal emulator的使用權(quán)轉(zhuǎn)回給自己,進(jìn)而shell又可以開始從terminal emulator接收新的命令了。
下面我們來看一些具體的例子:
上圖是用ps命令輸出的信息,看圖中的選中行,第一行為alacritty進(jìn)程,即我們最開始啟動的terminal emulator,第二行為alacritty啟動的子進(jìn)程,在該子進(jìn)程中,運(yùn)行的是bash程序,第三行為bash啟動的子進(jìn)程,在該子進(jìn)程中,運(yùn)行的是我們文章最開始時使用的測試程序hello。
這三個進(jìn)程的層級關(guān)系,和我們上面的描述是一致的。
我們再來看下alacritty啟動bash子進(jìn)程的相關(guān)代碼:
上圖中make_pty函數(shù)內(nèi)會調(diào)用openpty函數(shù)獲取master fd和slave fd,在獲取到master fd和slave fd后,slave fd被賦值到builder的stdin, stdout, stderr里,這樣,在下面執(zhí)行builder.spawn函數(shù)啟動shell子進(jìn)程時,其標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)錯誤輸出就都指向slave fd了。
另外,在上圖中,master fd被保存到了Pty里,并和其他信息一起返回給該函數(shù)的調(diào)用方,這樣,alacritty如果想要發(fā)送數(shù)據(jù)給shell時,就從Pty里獲取到master fd,然后將數(shù)據(jù)寫入到master fd里就好了。
0x04 Ctrl-C是如何處理的
上面我們講過,我們在terminal emulator中輸入的內(nèi)容,會被terminal emulator寫到內(nèi)核的pty數(shù)據(jù)通道中,進(jìn)而這些數(shù)據(jù)會被轉(zhuǎn)發(fā)給shell進(jìn)程,或者是在shell中運(yùn)行的子進(jìn)程。
那在terminal emulator里按Ctrl-C,也是這么處理的嗎?
首先,Ctrl-C確實(shí)是被當(dāng)作一個字符來處理的,且terminal emulator在接收到這個字符后,會直接寫入到內(nèi)核的pty數(shù)據(jù)通道,并不做特殊處理。
但是,在內(nèi)核的pty數(shù)據(jù)通道里,有一個組件叫做 line discipline,它會檢查要被傳輸?shù)淖址绻址髦邪珻trl-C,它就會把Ctrl-C這個特殊字符從字符流中移除掉,并生成一個SIGINT信號,發(fā)送給目標(biāo)進(jìn)程。
如果目標(biāo)進(jìn)程沒有捕獲該信號,內(nèi)核就會執(zhí)行該信號的默認(rèn)行為,即殺掉目標(biāo)進(jìn)程。
以下是生成SIGINT信號的內(nèi)核代碼:
上圖中,光標(biāo)所在行就是在判斷該字符是否是Ctrl-C,如果是,則發(fā)送SIGINT信號給目標(biāo)進(jìn)程。
由上圖我們還可以看到,其實(shí)不止Ctrl-C這個特殊字符會轉(zhuǎn)化成信號,QUIT字符Ctrl-,SUSP字符Ctrl-Z等,都會被轉(zhuǎn)化成對應(yīng)的信號。
0x05 精致全景圖
以上講了很多理論,下面我們來畫一幅圖,來梳理下完整流程。
首先,在terminal emulator啟動成功后,我們在其中輸入./hello命令,該命令沿著terminal emulator中的第一個輸出箭頭,即第一條虛線,經(jīng)由內(nèi)核pty數(shù)據(jù)通道,到達(dá)bash進(jìn)程的stdin。
然后,bash從標(biāo)準(zhǔn)輸入中讀取到要執(zhí)行的命令./hello,fork一個新的子進(jìn)程,并在子進(jìn)程中開始執(zhí)行hello程序,此時bash也把terminal emulator的使用權(quán)交給了hello進(jìn)程。
hello程序在開始運(yùn)行后,就嘗試從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù)。
接著,我們在terminal emulator中再輸入hello world,該數(shù)據(jù)會沿著terminal emulator中的第二個輸出箭頭,即第一條實(shí)線,經(jīng)由pty數(shù)據(jù)通道,到達(dá)hello進(jìn)程的stdin。
hello進(jìn)程從標(biāo)準(zhǔn)輸入中讀到hello world字符串,然后直接將其寫入到了標(biāo)準(zhǔn)輸出,該數(shù)據(jù)又經(jīng)由內(nèi)核pty數(shù)據(jù)通道,到達(dá)terminal emulator的master fd端。
terminal emulator從master fd中讀取到hello world字符串,并將其顯示在界面中。
hello進(jìn)程在寫完hello world字符串后,自己主動退出,bash檢測到hello進(jìn)程退出后,又把terminal emulator的使用權(quán)轉(zhuǎn)回給自己。
bash寫命令提示符>到標(biāo)準(zhǔn)輸出,該數(shù)據(jù)再經(jīng)由pty數(shù)據(jù)通道到達(dá)terminal emulator的master fd端。
terminal emulator從master fd中讀取到bash的命令提示符,并將其顯示在界面上,提示用戶可以輸入下一條命令了。
以上就是在terminal emulator中執(zhí)行hello程序的完整流程。
從上圖中我們還可以看到,假設(shè)我們在terminal emulator中按Ctrl-C,該數(shù)據(jù)在到達(dá)內(nèi)核pty數(shù)據(jù)通道時,line discipline組件會將其轉(zhuǎn)換成SIGINT信號,并發(fā)給目標(biāo)進(jìn)程。
這個也解答了我們此篇文章的疑問,現(xiàn)在你應(yīng)該豁然開朗了吧。
另外,內(nèi)核中 line discripline 組件的能力也是可以被修改的,比如我們可以修改成按Ctrl-B觸發(fā)SIGINT信號,甚至是直接關(guān)閉SIGINT信號的生成,具體方式,可以查看stty命令的man文檔。
審核編輯:劉清
-
觸發(fā)器
+關(guān)注
關(guān)注
14文章
2019瀏覽量
61387 -
模擬器
+關(guān)注
關(guān)注
2文章
884瀏覽量
43449 -
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21758 -
Shell
+關(guān)注
關(guān)注
1文章
366瀏覽量
23478 -
rust語言
+關(guān)注
關(guān)注
0文章
57瀏覽量
3031
原文標(biāo)題:為什么Ctrl-C會中斷當(dāng)前運(yùn)行程序
文章出處:【微信號:良許Linux,微信公眾號:良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
Linux C語言獲取當(dāng)前程序名稱的三種方式
![Linux <b class='flag-5'>C</b>語言獲取<b class='flag-5'>當(dāng)前程序</b>名稱的三種方式](https://file1.elecfans.com/web2/M00/95/C7/wKgZomTnDnWAM1EfAABnokwvdjc749.jpg)
這個程序怎樣通過布爾量退出循環(huán)?
運(yùn)行程序出問題啦!!!
Ctrl-C在使用OGL的應(yīng)用程序中不起作用是怎么樣?
步進(jìn)電機(jī)加速-勻速-減速運(yùn)行程序(C51源程序)
步進(jìn)電機(jī)加速-勻速-減速運(yùn)行程序(ASM)
為什么區(qū)塊鏈1.0不能運(yùn)行程序
在STVDCOSMIC在RAM中運(yùn)行代碼stm8 ram中運(yùn)行程序
![在STVDCOSMIC在RAM中<b class='flag-5'>運(yùn)行</b>代碼stm8 ram中<b class='flag-5'>運(yùn)行程序</b>](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
評論