解密TTY
本文內(nèi)容來(lái)自The TTY demystified?,講述了*NIX系統(tǒng)中TTY的歷史與工作原理,看完后解決了我很多疑惑,于是做此翻譯,與大家分享。
譯者:李秋豪?江家偉
審校:
V1.0 Sun May 13 12:42:01 CST 2018
一直以來(lái),TTY子系統(tǒng)都是Linux/Unix設(shè)計(jì)中的一個(gè)關(guān)鍵點(diǎn)。不幸的是,這種重要性通常都被忽略了,并且也很難找到相關(guān)的介紹性文章。我認(rèn)為,對(duì)Linux中TTYs的基礎(chǔ)知識(shí)理解應(yīng)是每一個(gè)開(kāi)發(fā)人員和高級(jí)使用者所必備的。
注意:你將閱讀到的東西并不是那么“優(yōu)雅”。事實(shí)上,盡管在用戶角度看非常實(shí)用,TTY子系統(tǒng)是由很多繁雜的東西和特殊情況組成的。為了理解它們的由來(lái),我們必須回到過(guò)去:
歷史
在1869年,證券報(bào)價(jià)機(jī)(stock ticker)被發(fā)明了。這是一臺(tái)由打字機(jī),一對(duì)長(zhǎng)電纜和一個(gè)自動(dòng)收錄機(jī)打印機(jī)組成的電動(dòng)機(jī)械機(jī)器,其目的是長(zhǎng)距離實(shí)時(shí)傳播股票的價(jià)格。這個(gè)概念逐漸演變成更快的基于ASCII的電傳機(jī)(teletype)。Teletypes曾經(jīng)在世界各地的大型網(wǎng)絡(luò)中連接,并被稱為T(mén)elex,其主要用于傳輸商業(yè)電報(bào),但此時(shí)尚未連接到任何計(jì)算機(jī)。
與此同時(shí),計(jì)算機(jī)(雖然還是又笨重又昂貴)也開(kāi)始支持多任務(wù)處理了,即能夠?qū)崟r(shí)和多個(gè)用戶進(jìn)行交互。當(dāng)命令行最終取代了古老的批處理模型后,teletypes被用作輸入和輸出設(shè)備,因?yàn)樗鼈冊(cè)谑袌?chǎng)上很容易買(mǎi)到。
但是在市場(chǎng)上有許多種電傳機(jī),它們的模型都略有不同,因此需要計(jì)算機(jī)在軟件層形成兼容。在UNIX世界中,使用的方法是讓操作系統(tǒng)內(nèi)核處理所有底層細(xì)節(jié),例如字長(zhǎng),波特率,流量控制,奇偶校驗(yàn),用于基本行編輯(rudimentary line)的控制代碼等等。而視頻終端(例如20世紀(jì)70年代后期出現(xiàn)的VT-100等)的光標(biāo)移動(dòng),彩色輸出和其他高級(jí)功能則留給了應(yīng)用層。
現(xiàn)在,物理電傳機(jī)和視頻終端實(shí)際上已經(jīng)滅絕了。除非你在訪問(wèn)博物館或者你是一個(gè)硬件愛(ài)好者,否則你看到的所有TTY都是模擬視頻終端,即軟件仿真出來(lái)的終端。但我們即將看到,這些遠(yuǎn)古的知識(shí)依然潛藏在現(xiàn)代TTY設(shè)計(jì)之中。
用例
如下圖所示,用戶在終端(terminal)打字(物理電傳機(jī)),該終端通過(guò)一對(duì)電纜連接到計(jì)算機(jī)上的UART(通用異步接收器和發(fā)送器)。操作系統(tǒng)中有一個(gè)UART驅(qū)動(dòng)程序,用于管理字節(jié)的物理傳輸,包括奇偶校驗(yàn)和流量控制。在一個(gè)原始的系統(tǒng)中,UART驅(qū)動(dòng)程序會(huì)將傳入的字節(jié)直接傳送給某個(gè)應(yīng)用程序進(jìn)程,但是這種方法將缺乏以下基本特征:
行編輯。大多數(shù)用戶都會(huì)在輸入時(shí)犯錯(cuò),所以退格鍵會(huì)很有用。這當(dāng)然可以由應(yīng)用程序本身來(lái)實(shí)現(xiàn),但是根據(jù)UNIX設(shè)計(jì)“哲學(xué)”,應(yīng)用程序應(yīng)盡可能保持簡(jiǎn)單。為了方便起見(jiàn),操作系統(tǒng)提供了一個(gè)編輯緩沖區(qū)和一些基本的編輯命令(退格,清除單個(gè)單詞,清除行,重新打印),這些命令在行規(guī)范(line discipline)內(nèi)默認(rèn)啟用。高級(jí)應(yīng)用程序可以通過(guò)將行規(guī)范設(shè)置為原始模式(raw?mode)而不是默認(rèn)的成熟或準(zhǔn)則模式(cooked?and?canonical)來(lái)禁用這些功能。大多數(shù)交互程序(編輯器,郵件客戶端,shell,及所有依賴curses或readline的程序)均以原始模式運(yùn)行,并自行處理所有的行編輯命令。行規(guī)范還包含字符回顯和回車(chē)換行(譯者注:\r\n?和?\n)間自動(dòng)轉(zhuǎn)換的選項(xiàng)。如果你喜歡,可以把它看作是一個(gè)原始的內(nèi)核級(jí)sed(1)。
另外,內(nèi)核提供了幾種不同的行規(guī)范。一次只能將其中一個(gè)連接到給定的串行設(shè)備。行規(guī)范的默認(rèn)規(guī)則稱為N_TTY(drivers/char/n_tty.c,如果你想繼續(xù)探索的話)。其他的規(guī)則被用于其他目的,例如管理數(shù)據(jù)包交換(ppp,IrDA,串行鼠標(biāo)),但這不在本文的討論范圍之內(nèi)。
會(huì)話(Session)管理。用戶可能想要同時(shí)運(yùn)行多個(gè)程序,并且一次只與其中一個(gè)交互。如果一個(gè)程序進(jìn)入無(wú)限循環(huán),用戶可能想要終止或掛起它。在后臺(tái)啟動(dòng)的程序應(yīng)該能夠獨(dú)立運(yùn)行,直到它們嘗試向終端寫(xiě)入(被掛起)。同樣,用戶的輸入應(yīng)該指向前臺(tái)程序。對(duì)于這些功能,操作系統(tǒng)是在TTY驅(qū)動(dòng)程序(?TTY driver?drivers/char/tty_io.c)中實(shí)現(xiàn)的。
在操作系統(tǒng)中,如果已經(jīng)進(jìn)程有執(zhí)行上下文,我們就說(shuō)它是“活著的”(有一個(gè)執(zhí)行上下文),這也意味著它可以獨(dú)立執(zhí)行操作。而TTY驅(qū)動(dòng)程序不是“活”的; 在面向?qū)ο蟮男g(shù)語(yǔ)中,TTY驅(qū)動(dòng)程序是被動(dòng)對(duì)象(passive object)。它有一些數(shù)據(jù)字段和一些方法,但讓它做某事的唯一方法是當(dāng)它的某個(gè)方法從別的進(jìn)程的上下文或內(nèi)核中斷處理程序中調(diào)用時(shí)。行規(guī)范(line discipline)同樣是一個(gè)被動(dòng)對(duì)象。
現(xiàn)在把它們放在一起看,UART驅(qū)動(dòng),行規(guī)范和TTY驅(qū)動(dòng)這個(gè)三元組就可以被稱為T(mén)TY設(shè)備,即我們常說(shuō)的TTY。用戶進(jìn)程可以通過(guò)在/dev下操作相應(yīng)的設(shè)備文件來(lái)影響任何TTY設(shè)備的行為。由于對(duì)設(shè)備文件寫(xiě)入權(quán)限是必需的,因此當(dāng)用戶登錄特定的TTY時(shí),該用戶必須成為設(shè)備文件的所有者——這通常由login(1)程序完成,該程序以root權(quán)限運(yùn)行。
上圖中的物理電線也可以是長(zhǎng)途電話線路(Modem),除了系統(tǒng)必須處理調(diào)制解調(diào)器掛斷的情況,這并沒(méi)有帶來(lái)其他的改變:
讓我們繼續(xù)討論典型的桌面系統(tǒng)。下圖是Linux控制臺(tái)的工作原理:
在上圖中,TTY驅(qū)動(dòng)和行規(guī)范的行為與前面的示例類(lèi)似,但不再有UART或物理終端。相反,軟件仿真出視頻終端(字符和圖形字符屬性幀緩沖器的復(fù)雜狀態(tài)機(jī)),并最終被渲染到VGA顯示器。
如果我們?cè)谟脩艨臻g也進(jìn)行終端仿真,情況會(huì)變得更加靈活(和抽象)。下圖是xterm(1)及其克隆的工作方式:
為了便于將終端仿真移入用戶空間,同時(shí)仍保持TTY子系統(tǒng)(會(huì)話管理和行規(guī)范)的完整,偽終端被發(fā)明了出來(lái)(pseudo terminal?或?pty?)。你可能已經(jīng)猜到,當(dāng)你開(kāi)始在偽終端中運(yùn)行偽終端時(shí),事情變得更加復(fù)雜,例如?screen(1)?或?ssh(1)。
現(xiàn)在讓我們退一步看看所有這些東西是如何和進(jìn)程聯(lián)系起來(lái)的。
進(jìn)程
Linux進(jìn)程可以處于下面狀態(tài)之一:
| D | 不可中斷睡眠(等待某個(gè)事件) |
| S | 可中斷睡眠(等待一些事件或者信號(hào)) |
| T | 停止(收到了工作管理信號(hào)或者進(jìn)程正在被調(diào)試器追蹤) |
| Z | 僵尸進(jìn)程(被它的父進(jìn)程終止但是沒(méi)有被回收的進(jìn)程) |
| R | 運(yùn)行或者可運(yùn)行(在運(yùn)行隊(duì)列中) |
通過(guò)運(yùn)行?ps l, 你可以看到哪個(gè)進(jìn)程正在運(yùn)行,以及哪個(gè)進(jìn)程正在睡眠。如果一個(gè)進(jìn)程處于睡眠狀態(tài),?WCHAN?列("wait channel", 等待隊(duì)列的名字)將會(huì)告訴你這個(gè)進(jìn)程正在等待哪個(gè)內(nèi)核事件。
$ ps l F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND 0 500 5942 5928 15 0 12916 1460 wait Ss pts/14 0:00 -/bin/bash 0 500 12235 5942 15 0 21004 3572 wait S+ pts/14 0:01 vim index.php 0 500 12580 12235 15 0 8080 1440 wait S+ pts/14 0:00 /bin/bash -c (ps l) >/tmp/v727757/1 2>&1 0 500 12581 12580 15 0 4412 824 - R+ pts/14 0:00 ps l"wait"等待隊(duì)列對(duì)應(yīng)于系統(tǒng)調(diào)用?wait(2)?,因此這個(gè)隊(duì)列中的進(jìn)程的子進(jìn)程不論什么時(shí)候改變了狀態(tài),它們都會(huì)被移入運(yùn)行狀態(tài)。有兩種睡眠狀態(tài):可中斷睡眠和不可中斷睡眠。可中斷睡眠(最常見(jiàn)的情況)意味著當(dāng)進(jìn)程在等待隊(duì)列中時(shí),它實(shí)際上也可能由于收到了一個(gè)信號(hào)而被移入運(yùn)行狀態(tài)。如果你深入到內(nèi)核源碼中,你將會(huì)發(fā)現(xiàn)每個(gè)處理等待事件的內(nèi)核源碼都會(huì)檢查在schedule()調(diào)用返回之后是否有待處理的信號(hào),如果有,就從系統(tǒng)調(diào)用wait(2)中返回。
在上面列出的?ps?結(jié)果中,?STAT?列展示了每個(gè)進(jìn)程的當(dāng)前狀態(tài)。這一列中可能會(huì)顯示一個(gè)或多個(gè)屬性或標(biāo)記:
| + | 這個(gè)進(jìn)程是前臺(tái)進(jìn)程組的一員 |
這些屬性被用于工作管理。
譯者注:我之前翻譯過(guò)兩篇有關(guān)于進(jìn)程標(biāo)志的文章,可參考
Linux 進(jìn)程狀態(tài)標(biāo)識(shí) Process State Definition
Linux 可運(yùn)行進(jìn)程 Runnable Process Definition
工作與會(huì)話管理
當(dāng)你按下?^Z?掛起程序或者使用?&?在后臺(tái)運(yùn)行程序時(shí),工作管理就發(fā)生了。一個(gè)工作(job)等同于一個(gè)進(jìn)程組。shell內(nèi)置的命令如?jobs,?fg?和?bg?可以用來(lái)管理一個(gè)會(huì)話(session)中的所有工作。每一個(gè)會(huì)話是由一個(gè)會(huì)話領(lǐng)導(dǎo)(session leader),即shell來(lái)管理的,它會(huì)利用復(fù)雜的協(xié)議,例如信號(hào)和一些系統(tǒng)調(diào)用和內(nèi)核打交道。
下面的例子解釋了進(jìn)程、工作、會(huì)話之間的關(guān)系。
下面的shell交互...
...對(duì)應(yīng)這些進(jìn)程...
...和這些內(nèi)核數(shù)據(jù)結(jié)構(gòu)
- TTY 驅(qū)動(dòng) (/dev/pts/0).
- pipe0
其中基本的思想是每個(gè)管道都是一項(xiàng)工作,因?yàn)楣艿乐械拿總€(gè)進(jìn)程都應(yīng)該被同時(shí)進(jìn)行操作(停止,恢復(fù),終止)。這也是為什么?kill(2)?允許你發(fā)送信號(hào)到整個(gè)進(jìn)程組。默認(rèn)情況下,?fork(2)?將新創(chuàng)建的子進(jìn)程放置在與其父進(jìn)程相同的進(jìn)程組中,例如,鍵盤(pán)上的?^C?會(huì)影響父進(jìn)程和子進(jìn)程。但是,作為會(huì)話領(lǐng)導(dǎo)責(zé)任的一部分,每次啟動(dòng)管道時(shí),shell都會(huì)創(chuàng)建一個(gè)新的進(jìn)程組。
TTY驅(qū)動(dòng)程序會(huì)記錄前臺(tái)進(jìn)程組ID(PID),但這只能以被動(dòng)方式進(jìn)行。會(huì)話領(lǐng)導(dǎo)必須在必要時(shí)主動(dòng)更新此信息。同樣,TTY驅(qū)動(dòng)程序會(huì)記錄連接終端的屬性(例如窗口大小),但這些信息必須由終端仿真程序甚至用戶主動(dòng)更新。
正如在上圖中所看到的,幾個(gè)進(jìn)程將?/dev/pts/0?作為它們的標(biāo)準(zhǔn)輸入。但只有前臺(tái)工作?ls | sort?才會(huì)接收來(lái)自TTY的輸入。同樣,只有前臺(tái)工作才被允許寫(xiě)入TTY設(shè)備(默認(rèn)配置下)。如果cat進(jìn)程試圖寫(xiě)入TTY,內(nèi)核將使用信號(hào)將它掛起。
信號(hào)控制
現(xiàn)在讓我們更近距離地看看內(nèi)核中的TTY驅(qū)動(dòng)、行規(guī)范和UART驅(qū)動(dòng)是如何和用戶態(tài)進(jìn)程交互的。
UNIX文件,包括TTY設(shè)備文件,可以被讀和寫(xiě),并且由于許多TTY相關(guān)的操作都已經(jīng)被定義,可以使用神奇的?ioctl(2)系統(tǒng)調(diào)用(UNIX的“瑞士軍刀”)進(jìn)行進(jìn)一步操作。但是,ioctl請(qǐng)求必須在進(jìn)程內(nèi)被初始化,因此它們不能在內(nèi)核需要和應(yīng)用進(jìn)行異步通信的場(chǎng)景下被使用。
在The Hitchhiker's Guide to the Galaxy(銀河系漫游指南)中,Douglas Adams提到了一個(gè)“死星”,上面居住這一群消沉的人類(lèi)和某種長(zhǎng)著尖牙的動(dòng)物。這些動(dòng)物通過(guò)狠狠地咬人類(lèi)的大腿來(lái)和人類(lèi)交流(譯者:喵喵喵?)。這和UNIX驚人地相似:在UNIX中,內(nèi)核通過(guò)發(fā)送“癱瘓或者致命”的信號(hào)給用戶進(jìn)程來(lái)和進(jìn)程通信。一些進(jìn)程可能能夠攔截一些信號(hào),并且嘗試調(diào)整適應(yīng)當(dāng)前的情況,但是大多數(shù)進(jìn)程不會(huì)這么做。
因此信號(hào)是一個(gè)“粗暴”的機(jī)制,它允許內(nèi)核和進(jìn)程進(jìn)行異步通信。UNIX中的信號(hào)定義是不規(guī)整或者不統(tǒng)一的;相反,每個(gè)信號(hào)都是獨(dú)特的,我們必須單獨(dú)研究它們。
你可以使用命令?kill -l?來(lái)看看你的系統(tǒng)實(shí)現(xiàn)了哪些命令。結(jié)果看起來(lái)像下面這樣:
$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX正如你看到的,信號(hào)被從1開(kāi)始的數(shù)字編號(hào)。然而當(dāng)它們被在掩碼中(例如在ps -s的輸出里)被使用時(shí),最低有效位對(duì)應(yīng)信號(hào)1。
這篇文章將會(huì)關(guān)注以下信號(hào):?SIGHUP,?SIGINT,?SIGQUIT,?SIGPIPE,?SIGCHLD,SIGSTOP,?SIGCONT,?SIGTSTP,?SIGTTIN,?SIGTTOU?以及SIGWINCH.
SIGHUP
- 默認(rèn)操作:?終止
- 可能的操作: 終止, 忽略, 函數(shù)調(diào)用
當(dāng)檢測(cè)到掛斷(hangup)條件時(shí),UART驅(qū)動(dòng)會(huì)將SIGHUP?發(fā)送到整個(gè)會(huì)話。通常情況下,這會(huì)殺死所有進(jìn)程。某些程序(如?nohup(1)?和?screen(1))會(huì)從其會(huì)話(和TTY)中分離,以便其子進(jìn)程不會(huì)注意到掛斷。
SIGINT
- 默認(rèn)操作:?終止
- 可能的操作: 終止, 忽略, 函數(shù)調(diào)用
如果輸入流中出現(xiàn)交互式注意(?interactive attention?)字符(通常為?^C,其代碼為ASCII碼3),那么SIGINT?就會(huì)由TTY驅(qū)動(dòng)發(fā)送到當(dāng)前的前臺(tái)工作,除非此配置已被關(guān)閉。任何有權(quán)訪問(wèn)TTY設(shè)備的人都可以更改交互式注意字符并開(kāi)關(guān)此配置; 此外,會(huì)話管理器會(huì)跟蹤每個(gè)工作的TTY配置,并在有工作切換時(shí)更新TTY。
SIGQUIT
- 默認(rèn)操作:?內(nèi)核轉(zhuǎn)儲(chǔ)(core dump)
- 可能的操作: 內(nèi)核轉(zhuǎn)儲(chǔ), 忽略, 函數(shù)調(diào)用
SIGQUIT?的工作方式和?SIGINT相似, 但是使用的字符是?^\?并且默認(rèn)操作不同。
SIGPIPE
- 默認(rèn)操作:?終止
- 可能的操作: 終止, 忽略, 函數(shù)調(diào)用
內(nèi)核會(huì)給每一個(gè)試圖往沒(méi)有讀取者的管道中寫(xiě)數(shù)據(jù)的進(jìn)程發(fā)送?SIGPIPE?信號(hào)。 這是很有用的,因?yàn)闆](méi)有這個(gè)信號(hào)的話,類(lèi)似?yes | head這樣的工作就永遠(yuǎn)不會(huì)停止了。
SIGCHLD
- 默認(rèn)操作:?忽略
- 可能的操作: 忽略, 函數(shù)調(diào)用
當(dāng)進(jìn)程死亡或更改狀態(tài)(停止/繼續(xù))時(shí),內(nèi)核會(huì)向其父進(jìn)程發(fā)送一個(gè)?SIGCHLD?。?SIGCHLD?信號(hào)攜帶著終止進(jìn)程的附加信息,即進(jìn)程標(biāo)識(shí),用戶標(biāo)識(shí),退出狀態(tài)(或終止信號(hào))以及一些執(zhí)行時(shí)間的統(tǒng)計(jì)信息。會(huì)話領(lǐng)導(dǎo)(shell)使用這個(gè)信號(hào)追蹤其工作。
SIGSTOP
- 默認(rèn)操作:?掛起
- 可能的操作: 掛起
該信號(hào)將無(wú)條件地掛起接收者,即其信號(hào)動(dòng)作不能被重新配置。要注意的是,在工作控制期間,SIGSTOP不會(huì)由內(nèi)核發(fā)送。相反,^Z?通常會(huì)觸發(fā)一個(gè)?SIGTSTP,它可以被應(yīng)用程序攔截。然后應(yīng)用程序可以進(jìn)行例如將光標(biāo)移動(dòng)到屏幕底部等操作,然后使用SIGSTOP將自己置于睡眠狀態(tài)。
SIGCONT
- 默認(rèn)操作:?喚醒
- 可能的操作: 喚醒, 喚醒 + 函數(shù)調(diào)用
SIGCONT?將“反掛起”(un-suspend,continue)一個(gè)停止的進(jìn)程。當(dāng)用戶調(diào)用fg命令時(shí),它會(huì)由shell發(fā)送出去。由于?SIGSTOP?不能被應(yīng)用程序攔截,因此意料之外的SIGCONT?信號(hào)可能表明該進(jìn)程在某段時(shí)間之前被掛起,然后被喚醒。
SIGTSTP
- 默認(rèn)操作:?掛起
- 可能的操作: 掛起, 忽略, 函數(shù)調(diào)用
SIGTSTP?與?SIGINT?和?SIGQUIT?的工作原理相似,但是它使用的是?^Z?字符,并且默認(rèn)的操作是掛起進(jìn)程。
SIGTTIN
- 默認(rèn)操作:?掛起
- 可能的操作: 掛起, 忽略, 函數(shù)調(diào)用
如果一個(gè)后臺(tái)工作中的進(jìn)程嘗試從TTY設(shè)備中進(jìn)行讀取,TTY會(huì)向整個(gè)工作(組)發(fā)送一個(gè)?SIGTTIN信號(hào),這通常會(huì)掛起這個(gè)工作。
SIGTTOU
- 默認(rèn)操作:?掛起
- 可能的操作: 掛起, 忽略, 函數(shù)調(diào)用
如果一個(gè)后臺(tái)工作中的進(jìn)程嘗試向TTY設(shè)備中進(jìn)行寫(xiě)入,TTY會(huì)向整個(gè)工作(組)發(fā)送一個(gè)?SIGTTIN信號(hào),這通常會(huì)掛起這個(gè)工作。這種行為可以通過(guò)配置TTY關(guān)閉。
SIGWINCH
- 默認(rèn)操作:?忽略
- 可能的操作: 忽略, 函數(shù)調(diào)用
如前所述,TTY設(shè)備會(huì)記錄終端的窗口大小,但這些信息需要手動(dòng)更新。只要發(fā)生這種更新,TTY設(shè)備就會(huì)向前臺(tái)工作發(fā)送?SIGWINCH?。行為良好的交互式應(yīng)用程序(例如編輯器)會(huì)對(duì)此作出反應(yīng),從TTY設(shè)備獲取新的終端窗口大小并重繪GUI。
譯者注:我之前翻譯過(guò)一篇有關(guān)于進(jìn)程和信號(hào)的文章,可參考
Linux 進(jìn)程與信號(hào)的概念和操作
一個(gè)例子
假設(shè)你正在編輯(基于終端的)編輯器中的文件。此時(shí)光標(biāo)位于屏幕中間的某個(gè)位置,編輯器正在執(zhí)行一些任務(wù),例如對(duì)大文件執(zhí)行搜索和替換操作。現(xiàn)在你按?^Z,由于行規(guī)范已被配置為攔截此字符(^Z?是一個(gè)單字節(jié),ASCII碼為26),因此你無(wú)需等待編輯器完成其任務(wù)然后從TTY設(shè)備開(kāi)始讀取。相反,行規(guī)范子系統(tǒng)會(huì)立即將?SIGTSTP?發(fā)送到前臺(tái)進(jìn)程組。該進(jìn)程組包含編輯器以及由其創(chuàng)建的任何子進(jìn)程。
編輯器為?SIGTSTP?安裝(install)了一個(gè)信號(hào)處理程序,因此內(nèi)核將程序執(zhí)行流轉(zhuǎn)移到信號(hào)處理程序代碼中。通過(guò)將相應(yīng)的控制序列寫(xiě)入TTY設(shè)備,該代碼將光標(biāo)移動(dòng)到屏幕的最后一行。由于編輯器仍處于前臺(tái),控制序列按要求發(fā)送。隨后編輯器會(huì)將?SIGSTOP?發(fā)送到其自己的進(jìn)程組(正如上節(jié)信號(hào)中說(shuō)的那樣)。
編輯器現(xiàn)在已經(jīng)停止,SIGCHLD?信號(hào)向會(huì)話領(lǐng)導(dǎo)通告這個(gè)事件,其中包括該進(jìn)程的ID。當(dāng)前臺(tái)工作中的所有進(jìn)程都被掛起時(shí),會(huì)話領(lǐng)導(dǎo)從TTY設(shè)備讀取當(dāng)前配置,并將其存儲(chǔ)起來(lái)以供以后使用。會(huì)話領(lǐng)導(dǎo)繼續(xù)使用?ioctl?調(diào)用將其自身安裝為T(mén)TY的當(dāng)前前臺(tái)進(jìn)程組。然后,它會(huì)打印類(lèi)似 "[1]+ Stopped" 的內(nèi)容,以通知用戶工作已暫停。
此時(shí),?ps(1)?會(huì)告訴你編輯器進(jìn)程處于停止?fàn)顟B(tài)(“T”)。如果我們?cè)噲D使用內(nèi)置shell命令bg或使用?kill(1)?向進(jìn)程發(fā)送?SIGCONT來(lái)喚醒它,編輯器將開(kāi)始執(zhí)行其?SIGCONT信號(hào)處理程序。而該處理程序會(huì)嘗試通過(guò)寫(xiě)入TTY設(shè)備來(lái)重新繪制編輯器的GUI界面。但現(xiàn)在編輯器是一個(gè)后臺(tái)工作,TTY設(shè)備將不允許它進(jìn)行寫(xiě)入。所以,TTY會(huì)給編輯器發(fā)送?SIGTTOU?信號(hào),令其再次停止。這個(gè)事件將通過(guò)使用?SIGCHLD傳遞給會(huì)話領(lǐng)導(dǎo)(shell),而shell會(huì)再次向終端寫(xiě)入“[1] + Stopped”。
但是,當(dāng)我們鍵入fg時(shí),shell首先恢復(fù)先前保存的行規(guī)范配置。它通知TTY驅(qū)動(dòng)編輯器工作應(yīng)該從現(xiàn)在起作為前臺(tái)工作。最后,它向進(jìn)程組發(fā)送一個(gè)SIGCONT?信號(hào)。編輯器試圖重繪它的GUI,這次它不會(huì)被SIGTTOU?中斷,因?yàn)樗F(xiàn)在是前臺(tái)工作的一部分。
譯者注:
流控制與I/O阻塞
在?xterm中運(yùn)行?yes?,你會(huì)看到很多“y”出現(xiàn)在你眼前。自然,yes進(jìn)程能夠很快的產(chǎn)生y,以至于xterm來(lái)不及進(jìn)行幀緩沖區(qū)更新,與X服務(wù)器通信(譯者注:X Window System)以便滾動(dòng)窗口等操作。那么這些程序是如何進(jìn)行配合的呢?
答案在于I/O阻塞。偽終端只能在其內(nèi)核緩沖區(qū)內(nèi)保存一定數(shù)量的數(shù)據(jù),當(dāng)該緩沖區(qū)滿并且?yes?嘗試調(diào)用?write(2)時(shí),write(2)將被阻止,并將yes?進(jìn)程移至可中斷的睡眠狀態(tài),直到xterm能夠讀取緩沖中的字節(jié)。
如果TTY連接到串行端口,也會(huì)發(fā)生同樣的情況。假設(shè)?yes?能夠以比9600波特的速率傳輸數(shù)據(jù),但是如果串行端口被限制在低的多速度上,內(nèi)核緩沖區(qū)很快就會(huì)被填滿,并且任何后續(xù)的?write(2)?調(diào)用都會(huì)導(dǎo)致進(jìn)程睡眠(或收到返回的錯(cuò)誤號(hào)?EAGAIN?,如果進(jìn)程要求非阻塞I/O的話)。
如果我告訴過(guò)你,即使內(nèi)核緩沖區(qū)中還有剩余空間,也可以主動(dòng)地將TTY置于阻塞狀態(tài),更進(jìn)一步的說(shuō),每個(gè)試圖?write(2)?到TTY的進(jìn)程都會(huì)自動(dòng)阻塞。那么這種功能的用途是什么?
假設(shè)我們正在以9600波特率的速度與一些舊的VT-100通信。我們剛剛發(fā)送了一個(gè)復(fù)雜的控制序列,要求終端滾動(dòng)顯示。此時(shí),終端會(huì)因執(zhí)行滾動(dòng)操作無(wú)法以9600波特的全速率接收新數(shù)據(jù)。實(shí)際上,UART仍然以9600波特運(yùn)行,但終端中沒(méi)有足夠的緩沖空間來(lái)保持接收字符。現(xiàn)在就是將TTY置于阻塞狀態(tài)的好時(shí)機(jī)。但是,我們?cè)撊绾螐慕K端做到這一點(diǎn)?
我們已經(jīng)看到,TTY設(shè)備可以被配置為給某些數(shù)據(jù)字節(jié)特殊的處理。例如,在默認(rèn)配置中,收到的?^C?字節(jié)不會(huì)通過(guò)read(2)傳遞給應(yīng)用程序,而是會(huì)將?SIGINT?信號(hào)傳遞到前臺(tái)工作。類(lèi)似地,可以將TTY配置為對(duì)停止流和開(kāi)始流做出反應(yīng),通常分別是?^S?(ASCII碼19)和?^Q?(ASCII碼17)。舊的硬件終端會(huì)自動(dòng)傳輸這些字節(jié),并期望操作系統(tǒng)相應(yīng)地調(diào)節(jié)其數(shù)據(jù)流。這被稱為流控制,這就是為什么當(dāng)你偶然按下?^S?時(shí),你的xterm?會(huì)“鎖定”。
這里有一個(gè)重要的區(qū)別:寫(xiě)入由于流控制而停止的TTY,或者由于缺少內(nèi)核緩沖區(qū)空間,只會(huì)阻塞你的進(jìn)程,而從后臺(tái)工作中寫(xiě)入TTY將導(dǎo)致SIGTTOU?暫停整個(gè)進(jìn)程組。我不知道為什么UNIX的設(shè)計(jì)師必須發(fā)明?SIGTTOU?和?SIGTTIN?,而不是僅僅依靠I/O阻塞,但我最好的猜測(cè)是負(fù)責(zé)工作控制的TTY驅(qū)動(dòng)是為了監(jiān)視和操縱整個(gè)工作——而不是其中的單個(gè)進(jìn)程。
配置TTY設(shè)備
為了找出你的shell調(diào)用的控制TTY,你可以使用前面說(shuō)過(guò)的ps l,或者運(yùn)行tty(1)命令。
進(jìn)程可以使用?ioctl(2)讀取或修改打開(kāi)的TTY設(shè)備的配置。 該API在?tty_ioctl(4)中有描述。 由于它是Linux應(yīng)用程序和內(nèi)核之間的二進(jìn)制接口的一部分,它將在Linux版本迭代中得到保持。 但是,該接口是不可移植的,應(yīng)用程序應(yīng)該使用?termios(3)?手冊(cè)頁(yè)中描述的POSIX包裝器。
我不會(huì)詳細(xì)討論?termios(3)?接口的細(xì)節(jié),但是如果你正在編寫(xiě)C程序并希望在?^C?變成?SIGINT之前攔截?^C?,或者禁用行規(guī)范或字符回顯,或?qū)⒏囊粋€(gè)串的口波特率,關(guān)閉流控制等,你就會(huì)發(fā)現(xiàn)你需要上述的手冊(cè)頁(yè)(man page)。
這里還有一個(gè)名為?stty(1)的命令行工具來(lái)操作TTY設(shè)備。 它使用的是?termios(3)?API。
讓我們?cè)囋嚢?#xff01;
$ stty -aspeed 38400 baud; rows 73; columns 238; line = 0;intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts-ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke-a?參數(shù)是讓stty顯示所有的設(shè)置。默認(rèn)情況下,它將查看連接到shell的TTY設(shè)備,但可以通過(guò)-F指定其他的設(shè)備。
在上面顯示出的設(shè)置中,一些會(huì)改變UART參數(shù),一些會(huì)影響行規(guī)范,一些則用于工作控制。我們先來(lái)看看第一行:
| rows, columns | TTY驅(qū)動(dòng) | 該TTY設(shè)備的終端的大小(以字符作為基準(zhǔn))。基本上,它只是內(nèi)核空間中的一對(duì)變量,你可以自由設(shè)置和獲取。設(shè)置它們將導(dǎo)致TTY驅(qū)動(dòng)程序向前臺(tái)工作發(fā)送SIGWINCH。 |
| line | 行規(guī)范 | 該TTY設(shè)備的行規(guī)范. 0 代表?N_TTY. 所有可用的數(shù)值在?/proc/tty/ldiscs中有列出. 使用未列出的數(shù)值等價(jià)于使用?N_TTY, 但是不要依賴于這一點(diǎn). |
| speed | UART | 波特率。偽終端忽略這個(gè)參數(shù)。 |
嘗試以下操作:啟動(dòng)一個(gè)?xterm。記下它的TTY設(shè)備(?tty命令獲得)及其窗口大小(由stty -a獲得)。接著在xterm中啟動(dòng)?vim?(或其他一些全屏終端應(yīng)用程序)。vim編輯器會(huì)向TTY設(shè)備查詢當(dāng)前的終端窗口大小,以此填充整個(gè)窗口。現(xiàn)在,從另一個(gè)shell窗口輸入:
stty -F X rows Y其中X是剛才獲得的TTY設(shè)備,Y是終端高度的一半。這將更新內(nèi)核內(nèi)存中的TTY數(shù)據(jù)結(jié)構(gòu),并向編輯器發(fā)送?SIGWINCH?,vim將使用可用窗口區(qū)域的上半部分重繪GUI。
stty -a?輸出的第二行列出了所有特殊的字符,開(kāi)一個(gè)新的?xterm?然后試試這個(gè):
stty intr o現(xiàn)在,"o"而不是?^C將向前臺(tái)工作發(fā)送?SIGINT?。嘗試運(yùn)行一些程序,比如?cat,并看看你能不能用?^C殺死它。然后,嘗試在其中輸入“hello”。
有時(shí)候,你可能會(huì)遇到退格鍵不起作用的Unix系統(tǒng)——當(dāng)終端仿真器發(fā)送與TTY設(shè)備中的擦除設(shè)置不匹配的退格碼(ASCII 8或ASCII 127)時(shí),就會(huì)發(fā)生這種情況。為了解決這個(gè)問(wèn)題,請(qǐng)?jiān)O(shè)置?stty erase ^H?(ASCII 8)或?stty erase ^??(ASCII 127)。要注意的是,許多終端應(yīng)用程序使用readline,這使得行規(guī)范處于原始模式,即這些應(yīng)用程序不受到影響。
最后,stty -a列出了一系列開(kāi)關(guān)(沒(méi)有特定順序列出)。其中一些與UART相關(guān),一些影響線路規(guī)范行為,一些用于流量控制,一些用于工作控制。短劃線( - )表示開(kāi)關(guān)關(guān)閉;否則它是開(kāi)著的。所有的開(kāi)關(guān)都在stty(1)手冊(cè)頁(yè)中進(jìn)行了解釋,所以我將簡(jiǎn)單地提一下:
icanon用于將行規(guī)范切換為規(guī)則(基于行)模式。在一個(gè)新的?xterm中試試這個(gè),關(guān)閉這個(gè)模式:
stty -icanon; cat現(xiàn)在所有的行編輯字符,例如退格或者^(guò)U都會(huì)停止工作。另外注意到cat?會(huì)一次接受一個(gè)字符(并連續(xù)輸出),而不是一次接受一行。
echo?是啟用字符回顯的開(kāi)關(guān)(默認(rèn)也是開(kāi)著的)。現(xiàn)在重新啟動(dòng)規(guī)則模式(stty icanon)然后試試這個(gè):
stty -echo; cat當(dāng)你輸入時(shí),你的終端仿真器將信息傳送給內(nèi)核,而內(nèi)核通常會(huì)將相同的信息回顯給終端仿真器,以便讓你看到之前鍵入的內(nèi)容。現(xiàn)在沒(méi)有了字符回顯,你就不能看到你輸入的內(nèi)容。不過(guò)我們處于熟化(cooked)模式,所以行編輯工具仍在工作。一旦你按下回車(chē)鍵,行規(guī)范就會(huì)把編輯緩沖區(qū)的數(shù)據(jù)傳送給cat,顯示出你剛剛鍵入的內(nèi)容。
tostop?是控制后臺(tái)進(jìn)程是否允許寫(xiě)入終端的開(kāi)關(guān),先試試這個(gè):
stty tostop; (sleep 5; echo hello, world) &&?會(huì)使得該命令作為后臺(tái)工作運(yùn)行。五秒鐘后,該工作將嘗試寫(xiě)入TTY。 TTY驅(qū)動(dòng)程序?qū)⑹褂?SIGTTOU將其掛起,并且shell可能會(huì)立即報(bào)告此事件,或者發(fā)出別的提示。現(xiàn)在嘗試下面的代碼:
stty -tostop; (sleep 5; echo hello, world) &五秒鐘之后,后臺(tái)工作會(huì)在你當(dāng)前的光標(biāo)位置輸出?hello, world?。
最后,?stty sane?會(huì)將你的TTY設(shè)置成一個(gè)相對(duì)合理的配置。
結(jié)語(yǔ)
我希望這篇文章為你提供了足夠的信息去了解TTY驅(qū)動(dòng)和行規(guī)范,以及它們與終端,行編輯和工作控制之間的關(guān)系。 更多細(xì)節(jié)可以在我提到的各種手冊(cè)頁(yè)以及glibc手冊(cè)(info libc,"Job Control")中找到。
最后,盡管我沒(méi)有足夠的時(shí)間來(lái)回答所有問(wèn)題,但我歡迎任何對(duì)本網(wǎng)站上的其他網(wǎng)頁(yè)提出的反饋意見(jiàn)。 謝謝閱讀!
總結(jié)
- 上一篇: Git 添加空文件夹的方法
- 下一篇: jdbc获取clob图片_jdbc方式读