簡(jiǎn)介: 本文就 CJKTTY 補(bǔ)丁如何讓 linux 虛擬終端顯示漢字的原理進(jìn)行了討論,為此介紹了 Linux 虛擬終端和其依賴的硬件的工作原理。過(guò)程中我們分析了 Linux 字符終端的不足之處,并向讀者介紹前沿的 Wayland system compositor 是什么以及為什么 需要它。
CJKTTY 補(bǔ)丁是什么,為什么我寫了它
當(dāng)你不使用 X 的時(shí)候,打開電腦,你就在使用虛擬終端。這么多年來(lái)它工作的很好,直到它來(lái)到了中國(guó)。包含中文字符的文件名無(wú)法正確顯示,中文文檔無(wú)法閱讀。當(dāng)然可以使用 X , 但是我為什么不能讓終端也能顯示漢字呢?如果在 X 下我能讓屏幕顯示漢字,終端下一定也能。為此我開始了 internet 上的搜尋。 我找到了 fbterm,這是個(gè)可以利用 /dev/fb0 實(shí)現(xiàn)的終端模擬器,和 XTERM 一樣,只不過(guò) XTERM 利用的 X 繪制文字,而 fbterm 直接寫入 /dev/fb0。
/dev/fb0 是什么?
幀緩沖區(qū)設(shè)備。幀緩沖區(qū)是一塊存儲(chǔ)區(qū)域(內(nèi)存或者顯存或者其他的輸出設(shè)備的存儲(chǔ)空間),內(nèi)核將其抽象為一個(gè)設(shè)備。通過(guò)訪問(wèn)該設(shè)備就能訪問(wèn)幀緩沖區(qū)。幀緩沖區(qū)的內(nèi)容既是屏幕映像。由輸出設(shè)備不停的掃描幀緩沖區(qū)生成顯示設(shè)備的控制信號(hào)。
然而我似乎總是忘記登錄后開啟 fbterm,對(duì)我來(lái)說(shuō),等看到亂碼的時(shí)候突然想起沒(méi)有開啟 fbterm 并不是那么愉快的經(jīng)歷。這只是其中一個(gè)缺點(diǎn)。fbterm 占用了幀緩沖區(qū)設(shè)備,導(dǎo)致 w3m 這類使用幀緩存繪制圖像的終端 www 瀏覽器不能正常工作。許多依賴幀緩沖區(qū)設(shè)備的終端程序都不能被 fbterm 良好的兼容。 于是我繼續(xù)尋找,找到了 youbest 寫的中文補(bǔ)丁。 我有個(gè)喜歡使用最新內(nèi)核的習(xí)慣,當(dāng)內(nèi)核升級(jí)導(dǎo)致 youbest 的補(bǔ)丁再也不能使用的時(shí)候,我開始到 youbest 發(fā)布補(bǔ)丁的頁(yè)面留言,希望 youbest 百忙中能修改一下他的補(bǔ)丁。
Youbest 似乎很忙,在新內(nèi)核的誘惑下,我決定自己在修改。那個(gè)時(shí)候我才真正的開始看內(nèi)核的代碼。終于了解了虛擬終端的工作原理后,我開始修改內(nèi)核。由于內(nèi)核內(nèi)部結(jié) 構(gòu)變動(dòng)導(dǎo)致 youbest 的補(bǔ)丁無(wú)法應(yīng)用,我?guī)缀跏菑念^開始了開發(fā)而不是簡(jiǎn)單的將無(wú)法應(yīng)用的部分進(jìn)行修整。唯一得到保留的就是 youbest 補(bǔ)丁中的點(diǎn)陣字庫(kù)。我將其命名為 CJKTTY , 取能顯示 CJK 的 TTY 之意。
從那里獲得中文顯示補(bǔ)丁呢?
我將補(bǔ)丁托管在了 http://repo.or.cz/w/linux-2.6/cjktty.git,依據(jù)你使用的內(nèi)核版本,簽出內(nèi)核版本 -utf8 分支就可以。
控制臺(tái)是如何顯示文字呢?
那么,為了能在控制臺(tái)下顯示出漢字到底需要做什么樣的修改么?在開始前,我想先介紹一些名字,并介紹一些控制臺(tái)在硬件上是如何進(jìn)行文字顯示的。 首先我解釋一下幾個(gè)名詞,知道的人可以到這里開始閱讀。
UNICODE
為每一個(gè)字符分配全球唯一的一個(gè)數(shù)字,但是并沒(méi)有規(guī)定這個(gè)數(shù)字的表示方法。數(shù)字的表示方法由 UTF 規(guī)范規(guī)定。UTF-16 使用 2 個(gè)字節(jié)表示一個(gè) UNICODE 數(shù)字,但是對(duì)于 >=216的數(shù)字使用 4 字節(jié)來(lái)表達(dá)。UTF-8 則對(duì)于 <127 的數(shù)字采取單字節(jié)表示,大于 127 的數(shù)字要根據(jù)其大小選擇 2~6 個(gè)字節(jié)進(jìn)行表示。UNICODE 在程序內(nèi)部則簡(jiǎn)單的使用 unsigned long 即可表示一個(gè)字符。
GLYPH
GLYPH 指的是字體里的字形。字符總是要在特定的字體下表示的,該表示就是字形。比如一個(gè)只包含 26 個(gè)大小寫字母的字體,只包含了 52 個(gè)字形,如果該字體是先大寫后小寫排列的,那么數(shù)字 0 就表示字形 A , 數(shù)字 1 就表示字形 B。UNICODE 或者 ASCII 到 GLYPH 的映射是由一個(gè)稱作 CMAP 的映射表做的。如果字體里字符就是按照 UNICODE 排列的,則其 CMAP 就是 UNICODE CMAP。同理也有 ASCII CMAP。 VGA 自帶字體沒(méi)有提供 CMAP,操作系統(tǒng)假定它的 CMAP 是 ASCII CMAP。事實(shí)上也是如此。
TTY
內(nèi)核為終端提供的接口,對(duì)應(yīng)用程序而言就是 TTY 設(shè)備。通常是使用 stdin stdout 來(lái)訪問(wèn)。TTY 提供各種 IOCTL 用來(lái)設(shè)置終端的模式。TTY 也提供了用戶控制程序的方法,比如 Ctrl-C 終止當(dāng)前程序。 TTY 可以是顯示器 + 鍵盤構(gòu)成的控制臺(tái),也可以是串口(可以通過(guò)貓鏈接到電話線上),可以通過(guò) pts 模擬。XTERM 即利用 pts 為里面運(yùn)行的程序提供的模擬的終端 , 對(duì)應(yīng)的設(shè)備文件 /dev/pts/* 由模擬終端程序動(dòng)態(tài)創(chuàng)建。
控制臺(tái) (CONSOLE)
控制臺(tái)特指由顯示器 + 鍵盤構(gòu)成的終端。其中顯示器由顯卡控制,而且當(dāng)前 VGA 兼容顯卡有兩種模式,文字模式和圖形模式。Linux 即可以使用文字模式也可以使用圖形模式。
控制臺(tái)對(duì)于程序是無(wú)法訪問(wèn)的,程序只能通過(guò)虛擬終端使用控制臺(tái)
虛擬終端 (VT)
如果你的電腦只有一個(gè)終端,那將是多么乏味。一個(gè)需要長(zhǎng)時(shí)間執(zhí)行的任務(wù)就能導(dǎo)致你什么也做不了,Linux 的多任務(wù)機(jī)制的好處蕩然無(wú)存。所以,你需要更多的終端。Linux 內(nèi)核使用復(fù)用機(jī)制,將一個(gè)控制臺(tái)復(fù)用為多個(gè)終端 (63 個(gè),/dev/tty1 到 dev/tty63)。 按鍵 Alt+F1-F12 ( 如果當(dāng)前在 X 中,需要再按下 Ctrl 鍵 ) 能在 12 個(gè)終端中進(jìn)行切換。事實(shí)上你擁有 63 個(gè)終端,鍵盤只能切換其中的 12 個(gè),其他的終端你可以通過(guò) chvt 命令進(jìn)行切換。
當(dāng)前擁有顯示器和鍵盤的虛擬終端被稱為活動(dòng)終端或者當(dāng)前終端。
TTY、控制臺(tái)和虛擬終端有啥區(qū)別和聯(lián)系?
當(dāng)你按下 Ctrl-C 的時(shí)候,當(dāng)前執(zhí)行的程序會(huì)被終止。因?yàn)?Linux 發(fā)送了 SIGTERM 信號(hào)給此終端的前臺(tái)程序。該信號(hào)并不是由 Shell 產(chǎn)生,而是內(nèi)核。不論是在虛擬終端下,還是在 X 里的終端模擬器里,這個(gè)功能都是一樣的。終端的一大功能就是進(jìn)行任務(wù)控制,另一個(gè)功能是輸入輸出。輸入輸出模式下,還可以選擇行編輯模式,回顯模式,設(shè)置 終端速率等等。不管你使用的是何種終端,這些功能都是存在的,因?yàn)樗麄兌际且粋€(gè)類型的設(shè)備。內(nèi)核將他們抽象為 TTY 設(shè)備。也就是說(shuō),應(yīng)用程序都是在和 TTY 這個(gè)抽象層打交道,而不是和具體的設(shè)備打交道。 能作為 TTY 的設(shè)備除了控制臺(tái)外,還有串口。將兩臺(tái)電腦的串口連接起來(lái),其中一臺(tái)電腦為串口打開登錄程序(執(zhí)行 /sbin/agetty ttyS0 38400),另一臺(tái)就能通過(guò)可以進(jìn)行串口通信的程序 ( 比如 putty、minicom) 登錄對(duì)方。 控制臺(tái)可以作為 TTY 設(shè)備,但是一臺(tái)電腦一般只有一個(gè)屏幕,也就使用一個(gè)控制臺(tái),所以 Linux 在控制臺(tái)和 TTY 之間加了一層虛擬終端。由虛擬終端將控制臺(tái)復(fù)用,這樣就可以使用多個(gè)終端而不是只有一個(gè)了。多個(gè)虛擬終端設(shè)備合作使用一個(gè)控制臺(tái)。 除了串口和虛擬終端,這些都是在內(nèi)核實(shí)現(xiàn)的 TTY 設(shè)備,內(nèi)核還提供了一個(gè)叫 PTY 的為終端設(shè)備,XTERM 之類的程序利用 PTY 提供的功能可以在程序里實(shí)現(xiàn) TTY 的功能。 那么,虛擬終端就是利用控制臺(tái)復(fù)用出了多個(gè) TTY 。TTY 邏輯由 TTY 子系統(tǒng)完成,復(fù)用邏輯由虛擬終端實(shí)現(xiàn),而具體的顯示則交給控制臺(tái)完成。如果說(shuō)這是一個(gè)觀察者模型的話,控制臺(tái)就是觀察者,它將虛擬終端的內(nèi)容呈現(xiàn)到屏幕 上。 在 Linux 下,控制臺(tái)分文字模式控制臺(tái)(vgacon)和圖形模式控制臺(tái) (fbcon)。
文字模式控制臺(tái) (VGA 文字模式 )
文字模式控制臺(tái)使用 VGA 兼容顯卡的文字模式實(shí)現(xiàn) VGA 兼容顯卡初始化時(shí)默認(rèn)就處于文字模式,能顯示 80x25 個(gè)字符。
在文字模式下,顯卡雖然輸出給顯示器的是圖像,但是顯卡提供給內(nèi)核的卻只能顯示文字功能。要顯示一個(gè)字符,內(nèi)核將要顯示的字符的代碼和屬性寫入字符緩沖區(qū)相應(yīng)地址即可。緩沖區(qū)如下面所示:
圖 1. 緩沖區(qū)

表 1. 每個(gè)字符的格式
屬性字節(jié) 字符字節(jié) 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 閃爍 背景色 前景色 GLYPH
VGA 顯卡處于文字模式時(shí),物理地址 0xB8000 即文字緩沖區(qū)起始地址,大小由其文字模式?jīng)Q定,最大 32KB 。VGA 顯卡內(nèi)建字符發(fā)生器,不斷的掃描字符緩沖區(qū)并將文字轉(zhuǎn)換為圖形驅(qū)動(dòng)顯示器顯示。其 GLYPH 即為字符的 ASCII 碼。
字符發(fā)生器和字體
字符發(fā)生器內(nèi)建一個(gè)或者多個(gè)位圖字體。使用何種字體取決于設(shè)置的文字模式代碼。Linux 默認(rèn)使用 80x25 字符 16 色模式 , 每字符 8x16 點(diǎn)陣。 字符發(fā)生器的字體包含 256 個(gè)字形。字符發(fā)生器里的文字和字符的對(duì)應(yīng)關(guān)系在 DOS 系統(tǒng)下被稱為代碼頁(yè)。顯卡自帶的被稱為 OEM 代碼頁(yè)。
字符發(fā)生器的字體可以修改。DOS 下通過(guò)修改代碼頁(yè)實(shí)現(xiàn)。Linux 下可以使用 setfont。
因?yàn)檫@一歷史原因,VGA 的文字模式最多同屏出現(xiàn) 256 種不同的字符。對(duì)于只有 26 個(gè)字母的拉丁文字綽綽有余,但是卻無(wú)法滿足擁有上萬(wàn)字符的中文。 文字模式控制臺(tái)由 vgacon (drivers/video/console/vgacon.c)實(shí)現(xiàn)。 VGA 還有不常使用的單色文字模式,起始地址為 0xB0000 。并且字符格式也有所不同,這里不再介紹。
圖形模式控制臺(tái)
圖形模式控制臺(tái)直接操作幀緩沖區(qū)顯示文字。幀緩沖區(qū)是一塊存儲(chǔ)區(qū)域(通常是在顯存中),其內(nèi)容就是顯示在屏幕上的圖像。顯卡直接將其內(nèi)容轉(zhuǎn)化為顯示器控制信號(hào)。對(duì)于程序而言,幀緩沖區(qū)就是屏幕。
圖像模式控制臺(tái)使用幀緩沖區(qū)作為屏幕輸出,要想使用圖像模式控制臺(tái),必須加載幀緩沖區(qū)驅(qū)動(dòng)并有相應(yīng)的硬件。
不同的顯示設(shè)備通常使用不同的幀緩沖區(qū)驅(qū)動(dòng),對(duì)于 VGA 兼容顯卡,除了能使用針對(duì)顯卡寫的幀緩沖區(qū)驅(qū)動(dòng) ( 如果有的話 ),還可以使用通用的 VESA 驅(qū)動(dòng)。 VESA 驅(qū)動(dòng)利用 VGA 兼容的顯卡的 BIOS 將顯卡轉(zhuǎn)入 VGA 圖形模式并設(shè)置期望的緩沖區(qū)模式,由顯示模式內(nèi)核參數(shù) vga= 給出。 VGA 圖形模式下,幀緩沖區(qū)的起始地址為 0xA0000 ,最大 64KB 大小。像素格式看顯示色深。典型的 256 色模式下每像素一個(gè)字節(jié)。 如果 BIOS 支持 VBE 擴(kuò)展,能設(shè)置更大的分辨率和色彩深度,幀緩沖區(qū)大小也會(huì)超過(guò) 64KB,因而起始地址也不再是 0xA0000,具體細(xì)節(jié)請(qǐng)參考 維基百科. 幀緩存模式下,Linux 內(nèi)核將需要顯示的字符首先轉(zhuǎn)換為位圖,然后將位圖字體寫入幀緩存即可變呈現(xiàn)于屏幕。 也就是說(shuō),圖形模式下,由內(nèi)核來(lái)承擔(dān)字符發(fā)生器的工作。理論上來(lái)說(shuō),這個(gè)內(nèi)核自帶的字符發(fā)生器將能支持顯示多于 255 種字符。但是我將在后面告訴讀者,因?yàn)闅v史原因,內(nèi)核實(shí)現(xiàn)的字符發(fā)生器和 VGA 的字符發(fā)生器有著一樣的缺點(diǎn)。 圖形控制臺(tái)由 fbcon(drivers/video/console/fbcon.c)實(shí)現(xiàn)。圖形控制臺(tái)由于需要生成位圖字形,需要帶上位圖字體,編譯內(nèi)核的時(shí)候至 少需要選擇一個(gè)字體。位圖字體在內(nèi)核是一個(gè)大數(shù)組,不同的字體 ( drivers/video/console/font_* ) 存儲(chǔ)在各自的數(shù)組中。
為何圖形模式控制臺(tái)也不能顯示漢字
要解釋圖形模式控制臺(tái)為何不能顯示漢字,首先我們來(lái)了解一下虛擬終端是怎么管理屏幕上的文字顯示的。 虛擬終端的實(shí)現(xiàn)在 drivers/tty/vt/vt.c 。代表虛擬終端的數(shù)據(jù)是 struct vc。
struct vc{
struct vc_data;
struct work_struct;
};
故而 struct vc_data 才是我們要的虛擬終端的定義。我們先來(lái)看看 struct vc_data 到底定義了什麼東西吧。
struct vc_data 的定義在 include/linux/console_struct.h, 定義摘錄如下,爲(wèi)了不延長(zhǎng)篇幅,有省略的部分:
struct vc_data {
struct tty_port port; /* Upper level data */
unsigned short vc_num; /* Console number */
unsigned int vc_cols; /* [#] Console size */
unsigned int vc_rows;
省略 ...
const struct consw *vc_sw;
unsigned short *vc_screenbuf; /* In-memory character/attribute buffer */
unsigned int vc_screenbuf_size;
省略 ...
};
其中 vc_screenbuf 存儲(chǔ)了虛擬終端要顯示在屏幕上的文字。 const struct consw *vc_sw 指向控制臺(tái)驅(qū)動(dòng)提供的函數(shù)。虛擬終端利用里面的函數(shù)指針調(diào)用相應(yīng)的操作,比如重繪屏幕,繪制一個(gè)字符等等。這些操作由 vgacon 和 fbcon 等控制臺(tái)驅(qū)動(dòng)實(shí)現(xiàn)。 當(dāng)你切換終端的時(shí)候,實(shí)際上就是把當(dāng)前終端設(shè)置為你要切換過(guò)去的終端,并且重新繪制當(dāng)前終端 vc_data->vc_screenbuf 存儲(chǔ)的內(nèi)容。 當(dāng)你從鍵盤輸入命令或者程序運(yùn)行過(guò)程中要輸出內(nèi)容的時(shí)候,虛擬終端首先將輸出的字符進(jìn)行編碼轉(zhuǎn)化,轉(zhuǎn)化為對(duì)于字符的 GLYPH 代碼,并且將 GLYPH 和當(dāng)前字符屬性結(jié)合,最后將合成結(jié)果寫入當(dāng)前光標(biāo)所處的位置。內(nèi)核中實(shí)際的算法要復(fù)雜的多,還牽涉到中斷,但是為了簡(jiǎn)單快遞的把我們關(guān)心的部分的核心表達(dá) 出來(lái),我使用一下偽代碼表示不那么嚴(yán)謹(jǐn)?shù)倪^(guò)程。希望了解全部的讀者可以自行查看內(nèi)核相關(guān)的代碼,主要代碼在 drivers/tty/vt.c 的 do_con_write() 中。
清單 1. 偽代碼
vc_write(vc_data * vc, const char * string, int count){
for( ; count ; ){
/* 和當(dāng)前編碼有關(guān),如果是 utf8 就以 utf8 方式解碼不是 utf8 就按照 擴(kuò)展的 ASCII 方式,也就是一個(gè)字節(jié)就是一個(gè)字母。*/
int glyph = next_char(vc->utf,&string,&count);
int c = vc_build_attribute(vc)|(vc_glyph_mask& glyph);
// 把當(dāng)前設(shè)置的前景色背景色等屬性結(jié)合 ,glyph 不能超過(guò)描述它的位段 ;
vc->vc_screenbuf[vc->vc_pos] = c; // 寫入當(dāng)前位置
update_pos(); // 更新當(dāng)前光標(biāo)位置
}
notify_redraw(vc); // 調(diào)用 cosole 這個(gè)觀察者重會(huì)屏幕
}
你也許想知道 vc_screenbuf 指向的緩沖區(qū)的格式到底是怎樣的,現(xiàn)在也可以回答 前面提 到過(guò)的為何圖像控制臺(tái)依然不能支持漢字顯示的問(wèn)題了:vc_screenbuf 的格式就是 VGA 文字模式時(shí)顯卡所使用的文字緩沖區(qū)格式。上述偽代碼中的 vc_glyph_mask 就是 0xFF, 也就是 glyph 被截?cái)嘀荒?8 比特長(zhǎng)度。 打從一開始,fbcon 就是按照 VGA 字符發(fā)生器設(shè)計(jì)的。因?yàn)楫?dāng) vc_screenbuf 的格式和 VGA 字符緩沖區(qū)的格式一致的時(shí)候,切換終端就可以只需要 memcpy ——快速的拷貝到 VGA 字符緩沖區(qū)就能實(shí)現(xiàn)“重繪”當(dāng)前終端。現(xiàn)在看來(lái)這種做法局限性非常大,但是單年 PC 性能還不夠強(qiáng)大的時(shí)候,能做到快速的重繪是非常重要的。 重繪例程中,notify_redraw(vc) 用偽代碼表示為
清單 2. notify_redraw(vc) 偽代碼
notify_redraw(vc_data * vc){
for(int rows = 0 ; rows < vc->rows ; rows ++){
unsigned short * current_line = &
vc->vc_screen_buf [vc_size_row * rows + vc->vc_visible_origin ] ;
// 在屏幕的第 row 行第 0 列繪制一行 current_line 指向的內(nèi)容共 vc->cols 個(gè)字符。
vc->vc_sw->con_puts(vc,current_line,vc->cols,0,rows);
}
}
vc_sw 是個(gè)由控制臺(tái)代碼提供的指針,類型為 structconsw * , 控制臺(tái)驅(qū)動(dòng)的初始化部分會(huì)使用 vt_unbind() 將自己綁定為虛擬終端使用的控制臺(tái),傳入的信息中就包括 vc_sw。 在 vgacon 中 vc_sw->con_puts 事實(shí)上就是將要顯示的內(nèi)容簡(jiǎn)單的拷貝到 VGA 的字符緩沖區(qū)。對(duì)于 fbcon 而言則要復(fù)雜的多。 fbcon 提供的 con_puts 將 glyph 作為一個(gè)下標(biāo)在 vc->vc_font 中找到對(duì)應(yīng)的位圖數(shù)據(jù),然后拷貝到幀緩沖區(qū)。 TTY-> 虛擬終端 -> 控制臺(tái) -> 屏幕的路徑用一幅圖片表達(dá)為:
圖 2. 路徑

圖形模式控制臺(tái)的改造
fbcon 將 glyph 作為下標(biāo)到 vc_font 中獲取位圖數(shù)據(jù),而 glyph 要么是一個(gè) unicode (vc->utf=1 的時(shí)候,當(dāng)然是被截?cái)嗟?8 個(gè)比特)要么就是擴(kuò)展的 ASCII 代碼。 由于擴(kuò)展 ASCII 只有 8 個(gè)比特位表示一個(gè)字符,所以只能請(qǐng)出 unicode 作為中文的數(shù)字表示。要想控制臺(tái)能支持漢字顯示,需要解決 3 個(gè)問(wèn)題:
必須使用 UTF-8 模式 ( 默認(rèn) vc->utf=1 即可 )
虛擬控制臺(tái)的 vc_screenbuf 必須修改以為 glyph 提供至少 16bit 的空間。
圖形控制臺(tái)需要 vc_font 包含更多的字符,不只是 255 個(gè),并提供代碼繪制雙倍寬度的中文字形,字體中的字符按照 UNICODE 排列,這樣 glyph 就是字符的 UNICODE 編碼。
修改虛擬控制臺(tái)
一開始,我的打算是 vc_screenbuf 修改為 unsigned long long* 類型,32bit 給字符屬性,分別表示 16bit 終端前景色和背景色。glyph 則擁有 31bit 的空間 , 因?yàn)闈h字的寬度為雙倍的英文字母 ,其中 1 bit 用來(lái)表示雙字符寬度。比如 我 會(huì)表達(dá)為 兩個(gè) 我,第二個(gè)我的最高位為 1:繪制任何字形的時(shí)候,只繪制字形的左半部分;如果發(fā)現(xiàn)最高位為 1 則繪制字體位圖中的右半部分。這樣同樣的繪制代碼可以適應(yīng)英文字母和漢字。 寫入 vc_screenbuf 的時(shí)候, 如果是雙倍寬度的字符,需要同時(shí)寫入兩份,第二份的最高位置 1 就可以。 但是 vc_screenbuf 的格式已經(jīng)被到處假定為每字符兩個(gè)字節(jié)。如此修改導(dǎo)致牽一發(fā)動(dòng)全身。許多艱澀難懂的代碼都依賴 vc_screenbuf 是 每字符兩個(gè)字節(jié)的設(shè)定,直接修改定義后,光是編譯器能直接檢測(cè)出來(lái)的就有百余個(gè)地方需要修改,還有更多的邏輯并不能被編譯器檢測(cè)出來(lái)。 如此修改的后果就是會(huì)出現(xiàn)許多隱晦的錯(cuò)誤,非常難于調(diào)式。掙扎后,為最終選擇了另一條道路 :
為漢字重新分配一塊 vc_unicode_screenbuf
vc_unicode_screenbuf 緊挨著 vc_screenbuf , 事實(shí)上 vc_screenbuf 在分配空間的時(shí)候,多分配了一倍的空間,多分配的空間充作 vc_unicode_screenbuf,因此 struct vc_data 里并沒(méi)有添加 vc_unicode_screenbuf 成員。 vc_unicode_screenbuf 同樣為每字符 2 個(gè)字節(jié),并不包含字符屬性,所以 2 個(gè)字節(jié)如數(shù)用來(lái)保存 glyph。vc_screenbuf 格式未變,所以 vgacon 不需要修改,這就減少了大量的工作量。 向 vc_screenbuf 寫入字符的時(shí)候,同時(shí)寫入一份到 vc_unicode_screenbuf 。如果是漢字,由于其 glyph 大于 254 , 所以 vc_screenbuf 的那兩個(gè)字符 ( 漢字雙倍寬度 ) 實(shí)際寫入的是 0xff 和 0xfe ( 故而上文提到是 glyph 大于 254 的字符 ,0xfe 被保留它用了 )。0xff 表示該字符的 glyph 要到 vc_unicode_screenbuf 提取,然后繪制左半部分;0xfe 表示該字符的 glyph 要到 vc_unicode_screenbuf 提取,然后繪制右半部分。對(duì)于 glyph 大于 254 但是又不是雙倍寬度的字符,就不需要 0xfe 作陪了。 比如屏幕上顯示的文字是黑底白字的 “牛 B” , vc_screenbuf 的內(nèi)容就是 “0x00ff, 0x0ffe, 0x0f42 ” , vc_unicode_screenbuf 的內(nèi)容則是 “牛 , 牛 ,b” 。這是因?yàn)橐粋€(gè)漢字為兩倍的英文字母寬度。在屏幕文字緩沖區(qū)上也必須占用兩個(gè)字符的位置。并且必須有一種機(jī)制能知道應(yīng)該繪制左半部分和右半部分,我使用的 就是 0xff 和 0xfe。
修改圖形控制臺(tái)繪制代碼
要修改的地方只有 3 個(gè)。
struct console_font 添加 charcount 成員。將主線內(nèi)核的字體設(shè)置為 charcount = 255。 主線內(nèi)核帶的字體都是 255 個(gè) glyph 的,所以沒(méi)有添加字符個(gè)數(shù)的必要。不過(guò)我們即將要添加的字體會(huì)有數(shù)萬(wàn)字符。
添加一個(gè)新的字體,復(fù)蓋 UNICODE BMP 基本區(qū)域的所有符號(hào)。
修改字符繪制代碼,添加 vc_unicode_screenbuf 的支持。
字符繪制代碼的修改比較繁瑣,代碼分布在 drivers/video/console/ 下的多個(gè)文件中。fbcon_putc(s) 由由 vc->vc_sw->con_putc(s) 調(diào)用, fbcon_putc(s) 轉(zhuǎn)而調(diào)用分散于 drivers/video/console/ 的多個(gè) puts 實(shí)現(xiàn)。因?yàn)榻K端要支持 console_rotate , decoration , timing , 故而每種模式下的繪制實(shí)現(xiàn)都是不同的。 我拿 drivers/video/console/bitblt.c 最常用的不傾斜、不加裝飾等的終端模式為例來(lái)講解繪圖部分的修改。 由于中文字體為 16x16 點(diǎn)陣,是對(duì)齊的字體,故而其繪制代碼為 bit_putcs_aligned() 原先的代碼以 glyph 為下標(biāo)到 vc->vc_font->data 獲得字體數(shù)據(jù),然后調(diào)用 fb_pad_aligned_buffer 執(zhí)行塊拷貝操作。 我的修改很簡(jiǎn)單,原來(lái)獲得字體數(shù)據(jù)的代碼修改后放入 font_bits() 輔助函數(shù)。 在 font_bits 里,要判斷 glyph 是否為 0xff 或者 0xfe, 如果不是,使用 glyph 為下標(biāo)獲得字體的左半部分后并返回。 如果是,則從 vc_unicode_screenbuf 獲得真正的 glyph 數(shù)值,然后再依據(jù)現(xiàn)有的 glyph 是 0xff 還是 0xfe 去獲得字體的右半部分還是左半部分返回。font_bits 獲得字體數(shù)據(jù)后執(zhí)行 fb_pad_aligned_buffer 塊拷貝。 需要修改的地方還有 drivers/video/console/fbcon_ccw.c fbcon_cw.c fbcon_ub.c 。依原理進(jìn)行修改即可。
虛擬終端的不足之處
雖然費(fèi)盡心機(jī)添加了中文支持,那只是一個(gè) workaround , 并不能算真正的支持。要真正的支持必須徹底重寫虛擬終端和控制臺(tái)。而要支持中文,就需要更進(jìn)一步,全面支持 UNICODE , 包括支持從右向左的書寫習(xí)慣。 在內(nèi)核里實(shí)現(xiàn)一個(gè)全面支持 UNICODE 的控制臺(tái)并不是一件容易的事情,何況內(nèi)核的政策也不允許將如此龐大的字庫(kù)裝入內(nèi)核。于是乎,這里出現(xiàn)了死胡同。KMS 和 Wayland 的出現(xiàn)讓這死胡同似乎有了個(gè)完美的解。
KMS:
KMS 是內(nèi)核模式設(shè)置 (Kernel Mode Setting)的縮寫。傳統(tǒng)上內(nèi)核使用 VGA 模式,該模式由 BIOS 或者 bootloader 設(shè)置。如果啟動(dòng) Xorg, 則 Xorg 使用自己的驅(qū)動(dòng)將顯示模式進(jìn)行切換。這導(dǎo)致內(nèi)核并不知道顯卡的當(dāng)前工作狀態(tài),虛擬終端切換必須依賴 X 進(jìn)行。X 鎖死會(huì)導(dǎo)致整個(gè)終端被鎖定無(wú)法進(jìn)行切換。待機(jī)、休眠等功能必須依靠 X 和內(nèi)核雙方進(jìn)行深度合作才能實(shí)現(xiàn)。讓一個(gè)用戶程序搞垮內(nèi)核是不可以接受的,故有 KMS , 希望通過(guò)把模式設(shè)置代碼移入內(nèi)核,減少內(nèi)核對(duì) X 的依賴。
如果不使用 X , KMS 對(duì)于控制臺(tái)來(lái)說(shuō)就是支持了顯示器的本地本分辨率。KMS 優(yōu)勢(shì)并不顯著。但是 Wayland 的介入使事情發(fā)生了變化。 有關(guān)于 Wayland 的詳細(xì)文檔請(qǐng)參考 freedesktop.org 上的 項(xiàng)目首頁(yè)。Wayland 并不只是對(duì)桌面帶來(lái)了福音,同時(shí)也為控制臺(tái)帶來(lái)了福音,因?yàn)?Wayland 可以代替內(nèi)核自身的虛擬終端和控制臺(tái)實(shí)現(xiàn)。 而這個(gè)代替者就是 System Compositor
System Compositor?
System Compositor 是一個(gè) wayland compositor,只是運(yùn)行于系統(tǒng)全局范圍。
為了懶人我這里稍微講解一下 wayland compositor 吧。 Wayland 不同于 X , 在 wayland 的世界里,只有 compositor 和 client。Client 利用各種 API (wayland 給出的示例使用的是 OpenGL ES, 但其實(shí) wayland 并不限制使用的繪圖 API 類型 ) 進(jìn)行窗口繪圖,然后將窗口的繪制結(jié)果直接提交給 compositor 合成到屏幕上。這樣 wayland 本身就不包含繪圖 API 而大大簡(jiǎn)化了 wayland 的設(shè)計(jì)。Wayland compositor 可以同 X 一樣操作顯卡向屏幕輸出合成后的結(jié)果,也可以作為另一個(gè) wayland compositor 的 client。
對(duì)于多賬戶同時(shí)登錄的實(shí)現(xiàn),固然可以讓每一個(gè)本地 GUI 會(huì)話開啟一個(gè) wayland compositor,但是存在更好的辦法就是固定開啟一個(gè) system compositor。而讓所有用戶會(huì)話的 wayland compositor 再作為 system compositor 的 client. 藉由 system compositor 的合成效果,進(jìn)行快速用戶切換也可以進(jìn)行一些視覺(jué)效果。而且 Xorg 本身也已經(jīng)支持作為 wayland client 運(yùn)行,這樣可以使用傳統(tǒng)的 X 提供桌面,而讓 wayland system compositor 實(shí)現(xiàn)終端切換。 這還有一個(gè)好處,只有 wayland system compositor 是以 root 運(yùn)行的,而用戶會(huì)話的 compositor 或 X 就不必以 root 權(quán)限運(yùn)行。 因?yàn)?Wayland 非常輕量,所以 system compositor 可以作為系統(tǒng)級(jí)服務(wù)常駐內(nèi)存運(yùn)行。而因?yàn)橛辛?system compositor , 內(nèi)核也不再需要實(shí)現(xiàn)虛擬終端了:只需要實(shí)現(xiàn)終端模擬器作為 system compositor 的 client 。由于是在用戶空間實(shí)現(xiàn)的,所有可以加入 UNICODE,矢量字體,國(guó)際化的書寫習(xí)慣等等的支持,再也不用受限于內(nèi)核啦。 Wayland 還是一個(gè)非常年輕的項(xiàng)目,Wayland system compositor 目前還只是設(shè)想中的概念,需要更多的人關(guān)注參與。筆者相信不久的將來(lái) wayland 一定能大有作為。
寫在最后
本篇簡(jiǎn)要介紹了 Linux 虛擬終端的工作原理和依賴的硬件細(xì)節(jié)實(shí)現(xiàn),然后使用了不太優(yōu)雅的辦法讓虛擬終端在幀緩存模式下實(shí)現(xiàn)漢字的顯示。讓大家對(duì)虛擬終端有了一點(diǎn)點(diǎn)更多的了解,希 望本文能對(duì)想了解 Linux 的人有所幫助。也再次感謝 IBM DevelopWorks, 它讓我有機(jī)會(huì)把自己的知識(shí)共享給更多的人知道。
新聞熱點(diǎn)
疑難解答
圖片精選