基于Linux核心的漢字顯示的嘗試(frambuffer分析)
在闡述基于Linux核心的漢字顯示的技術(shù)細(xì)節(jié)之前,有必要介紹一下原有l(wèi)inux的工作機(jī)制。這里主要涉及到兩部分的知識(shí),就是Linux下終端和幀緩沖的實(shí)現(xiàn).
控制臺(tái)(console)
通常我們?cè)趌inux下看到的控制臺(tái)(console)是由幾個(gè)設(shè)備完成的。分別是/dev/ttyN(其中tty0就是/dev/console,tty1,tty2就是不同的虛擬終端(virtual console)).通常使用熱鍵alt Fn來(lái)在這些虛擬終端之間進(jìn)行切換。所有的這些tty設(shè)備都是由linux/drivers/char/console.c和vt.c對(duì)應(yīng)。其中console.c負(fù)責(zé)繪制屏幕上的字符,vt.c負(fù)責(zé)管理不同的虛擬終端,并且負(fù)責(zé)提供console.c需要繪制的內(nèi)容。Vt.c把不同虛擬終端下需要交給console.c繪制的內(nèi)容放到不同的緩存中去。Vt.c管理著這樣一個(gè)緩沖區(qū)的數(shù)組,并且負(fù)責(zé)在其間切換,以指定哪一個(gè)緩沖區(qū)是被激活的。你所看到的虛擬終端就對(duì)應(yīng)著被激活的緩沖區(qū)。Console.c同時(shí)也負(fù)責(zé)接收終端的輸入,然后把接收到的輸入放到緩沖區(qū)。
幀緩沖(framebuffer)
Framebuffer是把顯存抽象后的一種設(shè)備,可以通過(guò)這個(gè)設(shè)備的讀寫(xiě)直接對(duì)顯存進(jìn)行操作。這種操作是抽象的,統(tǒng)一的。用戶不必關(guān)心物理顯存的位置、換頁(yè)機(jī)制等等具體細(xì)節(jié)。這些都是由Framebuffer設(shè)備驅(qū)動(dòng)來(lái)完成的。
Framebuffer對(duì)應(yīng)的源文件在linux/drivers/video/目錄下。總的抽象設(shè)備文件為fbcon.c,在這個(gè)目錄下還有與各種顯卡驅(qū)動(dòng)相關(guān)的源文件。在使用幀緩沖時(shí),Linux是將顯卡置于圖形模式下的.
試驗(yàn)
我們以一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明字符顯示的過(guò)程。我們假設(shè)是在虛擬終端1(/dev/tty1)下運(yùn)行一個(gè)如下的簡(jiǎn)單程序。
main ( )
{
puts("hello, world.n");
}
puts函數(shù)向缺省輸出文件(/dev/tty1)發(fā)出寫(xiě)的系統(tǒng)調(diào)用write(2)。系統(tǒng)調(diào)用到linux核心里面對(duì)應(yīng)的核心函數(shù)是console.c中的con_write(),con_write()最終會(huì)調(diào)用do_con_write( )。在do_con_write( )中負(fù)責(zé)把"hello, world.n"這個(gè)字符串放到tty1對(duì)應(yīng)的緩沖區(qū)中去。
do_con_write( )還負(fù)責(zé)處理控制字符和光標(biāo)的位置。讓我們來(lái)看一下do_con_write()這個(gè)函數(shù)的聲明。
static int do_con_write(struct tty_struct * tty, int
from_user, const unsigned char *buf, int count) 其中tty是指向tty_struct結(jié)構(gòu)的指針,這個(gè)結(jié)構(gòu)里面存放著關(guān)于這個(gè)tty的所有信息(請(qǐng)參照l(shuí)inux/include/linux/tty.h)。Tty_struct結(jié)構(gòu)中定義了通用(或高層)tty的屬性(例如寬度和高度等)。
在do_con_write( )函數(shù)中用到了tty_struct結(jié)構(gòu)中的driver_data變量。
driver_data是一個(gè)vt_struct指針。在vt_struct結(jié)構(gòu)中包含這個(gè)tty的序列號(hào)(我們正使用tty1,所以這個(gè)序號(hào)為1)。Vt_struct結(jié)構(gòu)中有一個(gè)vc結(jié)構(gòu)的數(shù)組vc_cons,這個(gè)數(shù)組就是各虛擬終端的私有數(shù)據(jù)。
static int do_con_write(struct tty_struct * tty, int
from_user,const unsigned char *buf, int count)
{
struct vt_struct *vt = (struct vt_struct *)tty->
driver_data;/我們用到了driver_data變量
. . . . .
currcons = vt->vc_num; file:/我們?cè)谶@里的vc_nums就是1
. . . . .
}
要訪問(wèn)虛擬終端的私有數(shù)據(jù),需使用vc_cons〔currcons〕.d指針。這個(gè)指針指向的結(jié)構(gòu)含有當(dāng)前虛擬終端上光標(biāo)的位置、緩沖區(qū)的起始地址、緩沖區(qū)大小等等。
"hello, world.n"中的每一個(gè)字符都要經(jīng)過(guò)conv_uni_to_pc( )
這個(gè)函數(shù)轉(zhuǎn)換成8位的顯示字符。這要做的主要目的是使不同語(yǔ)言的國(guó)家能把16位的UniCode碼映射到8位的顯示字符集上,目前還是主要針對(duì)歐洲國(guó)家的語(yǔ)言,映射結(jié)果為8位,不包含對(duì)雙字節(jié)(double byte)的范圍。
這種UNICODE到顯示字符的映射關(guān)系可以由用戶自行定義。在缺省的映射表上,會(huì)把中文的字符映射到其他的字符上,這是我們不希望看到也是不需要的。所以我們有兩個(gè)選擇∶
1不進(jìn)行conv_uni_to_pc( )的轉(zhuǎn)換。
2加載符合雙字節(jié)處理的映射關(guān)系,即對(duì)非控制字符進(jìn)行1對(duì)1的不變映射。我們自己定制的符合這種映射關(guān)系的UNICODE碼表是direct.uni。
要想查看/裝載當(dāng)前系統(tǒng)的unicode映射表,可使外部命令loadunimap。
經(jīng)過(guò)conv_uni_to_pc( )轉(zhuǎn)換之后,"hello, world.n"中的字符被一個(gè)一個(gè)地填寫(xiě)到tty1的緩沖區(qū)中。然后do_con_write( )調(diào)用下層的驅(qū)動(dòng),把緩沖區(qū)中的內(nèi)容輸出到顯示器上(也就相當(dāng)于把緩沖區(qū)的內(nèi)容拷貝到VGA顯存中去)。
sw->con_putcs(vc_cons〔currcons〕.d, (u16 *)draw_from, (u16
*)draw_to-(u16 *)draw_from, y, draw_x);
之所以要調(diào)用底層驅(qū)動(dòng),是因?yàn)榇嬖诓煌娘@示設(shè)備,其對(duì)應(yīng)VGA顯存的存取方式也不一樣。
上面的Sw->con_putcs( )就會(huì)調(diào)用到fbcon.c中的fbcon_putcs()函數(shù)(con_putcs是一個(gè)函數(shù)的指針,在Framebuffer模式下指向fbcon_putcs()函數(shù))。也就是說(shuō)在do_con_write( )函數(shù)中是直接調(diào)用了fbcon_putcs()函數(shù)來(lái)進(jìn)行字符的繪制。比如說(shuō)在256色模式下,真正負(fù)責(zé)輸出的函數(shù)是void fbcon_cfb8_putcs(struct vc_data *conp, struct display *p,const unsigned short *s, int count, int
yy, int xx)
顯示中文
比如說(shuō)我們?cè)噲D輸出一句中文∶putcs(你好n );(你好的內(nèi)碼為0xc4,0xe3,0xba,0xc3)。這時(shí)候會(huì)怎么樣呢,有一點(diǎn)可以肯定,"你好"肯定不會(huì)出現(xiàn)在屏幕上,原因有∶核心中沒(méi)有漢字字庫(kù),中文顯示就是無(wú)米之炊了.
1在負(fù)責(zé)字符顯示的void fbcon_cfb8_putcs( )函數(shù)中,原有操作如下∶對(duì)于每個(gè)要顯示的字符,依次從虛擬終端緩沖區(qū)中以WORD為單位讀?。ǖ臀蛔止?jié)是ASCII碼,高8位是字符的屬性),由于漢字是雙字節(jié)編碼方式,所以這種操作是不可能顯示出漢字的,只能顯示出xxxx_putcs()是一個(gè)一個(gè)VGA字符.
要解決的問(wèn)題∶
確保在do_con_write( )時(shí)uni□pc轉(zhuǎn)換不會(huì)改變?cè)芯幋a。一個(gè)很直接的實(shí)現(xiàn)方式就是加載一個(gè)我們自己定制的UNICODE映射表,loadunimapdirect.uni,或者直接把direct.uni置為核心的缺省映射表。
針對(duì)如上問(wèn)題,我們要做的第一個(gè)嘗試方案是如下。
首先需要在核心中加載漢字字庫(kù),然后修改fbcon_cfb8_putcs()函數(shù),在fbcon_cfb8_putcs( )中一次讀兩個(gè)WORD,檢查這兩個(gè)WORD的低位字節(jié)是否能拼成一個(gè)漢字,如果發(fā)現(xiàn)能拼成一個(gè)漢字,就算出這個(gè)漢字在漢字字庫(kù)中的偏移,然后把它當(dāng)成一個(gè)16 x 16的VGA字符來(lái)顯示。
試驗(yàn)的結(jié)果表明∶
1能夠輸出漢字,但仍有許多不理想的地方,比如說(shuō),輸出以半個(gè)漢字開(kāi)始的一串漢字,則這半個(gè)漢字后面的漢字都會(huì)是亂碼。這是半個(gè)漢字的問(wèn)題。
2光標(biāo)移動(dòng)會(huì)破壞漢字的顯示。表現(xiàn)為,光標(biāo)移動(dòng)過(guò)的漢字會(huì)變成亂碼。這是因?yàn)楣鈽?biāo)的更新是通過(guò)xxxx_putc( )函數(shù)來(lái)完成的。
xxxx_putc( )函數(shù)與xxxx_putcs( )函數(shù)實(shí)現(xiàn)的功能類似,但是xxxx_putc()函數(shù)只刷新一個(gè)字符而不是一個(gè)字符串,因而xxxx_putc()的輸入?yún)?shù)是一個(gè)整數(shù),而不是一個(gè)字符串的地址。Xxxx_putc( )函數(shù)的聲明如下∶void fbcon_cfb8_putc(struct vc_data *conp, struct display *p, int c, int yy, int xx)
下一個(gè)嘗試方案就是同時(shí)修改xxxx_putcs( )函數(shù)和xxxx_putc()函數(shù)。為了解決半個(gè)漢字的問(wèn)題,每一次輸出之前,都從屏幕當(dāng)前行的起始位置開(kāi)始掃描,以確定要輸出的字符是否落在半個(gè)漢字的位置上。如果是半個(gè)漢字的位置,則進(jìn)行相應(yīng)的調(diào)整,即從向前移動(dòng)一
個(gè)字節(jié)的位置開(kāi)始輸出。
這個(gè)方案有一個(gè)困難,即xxxx_putc( )函數(shù)不用緩沖區(qū)的地址,而是用一個(gè)整數(shù)作為參數(shù)。所以xxxx_putc( )無(wú)法直接利用相鄰的字符來(lái)判別該定符是否是漢字。
解決方案是,利用xxxx_putc( )的光標(biāo)位置參數(shù)(yy, xx),可以逆推出該字符在緩沖區(qū)中的位置。但仍有一些小麻煩,在Linux的虛擬終端下,用戶可能會(huì)上卷該屏幕(shift pageup),導(dǎo)致光標(biāo)的y座標(biāo)和相應(yīng)字符在緩沖區(qū)的行數(shù)不一致。相應(yīng)的解決方案是,在逆推的過(guò)程中,考慮卷屏的參量。
這樣一來(lái),我們就又進(jìn)了一步,得到了一個(gè)相對(duì)更好的版本。但仍有問(wèn)題沒(méi)有解決。敲入turbonetcfg,會(huì)發(fā)現(xiàn)菜單的邊框字符也被當(dāng)成漢字顯示。這是因?yàn)椋@種邊框字符是擴(kuò)展字符,也使用了字符的第8位,因而被當(dāng)作漢字來(lái)顯示。例如,單線一的制表符內(nèi)碼為0xC4,當(dāng)連成一條長(zhǎng)線就是由一連串0xC4組成,而0xC4C4正是漢字哪。于是水平的制表符被一連串的哪字替代了。要解決這個(gè)問(wèn)題就非常不容易了,因?yàn)橹票矸姆N類比較多,而且垂直制表符與其后面字符的組合型式又多種多樣,因而很難判斷出相應(yīng)位置的字符是不是制表符,從理論上說(shuō),無(wú)論采取什么樣的排除算法,都必然存在誤判的情況,因?yàn)榭偞嬖诙x性,沒(méi)有充足的條件來(lái)推斷出當(dāng)前字符究竟是制表符還是漢字。
我們一方面尋找更好的排除組合算法,一方面試圖尋找其它的解決方案。要想從根本上解決定個(gè)問(wèn)題,必須利用其它的輔助信息,僅僅從緩沖區(qū)的字符來(lái)判斷是不夠的。
經(jīng)過(guò)一番努力,我們發(fā)現(xiàn),在UNIX中使用擴(kuò)展字符時(shí),都要先輸出字符轉(zhuǎn)義序列(Escape sequence)來(lái)切換當(dāng)前字符集。字符轉(zhuǎn)義序列是以控制字符Esc為首的控制命令,在UNIX的虛擬終端中完成終端控制命令,這種命令包括,移動(dòng)光標(biāo)座標(biāo)、卷屏、刪除、切換字符集等等。也就是說(shuō)在輸出代表制表符的字符串之前,通常是要先輸出特定的字符轉(zhuǎn)義序列。在console.c里,有根據(jù)字符轉(zhuǎn)義序列命令來(lái)記錄字符狀態(tài)的變量。結(jié)合該變量提供的信息,就可以非常干凈地把制表符與漢字區(qū)別開(kāi)來(lái)。
在如上思路的指引下,我們又產(chǎn)生了新的解決方案。經(jīng)過(guò)改動(dòng)得到了另一各版本.
在這個(gè)新版本上,turbonetcfg在初次繪制的時(shí)候,制表符與漢字被清晰地區(qū)分開(kāi)來(lái),結(jié)果是非常正確的。但還有新的問(wèn)題存在∶turbonetcfg在重繪的時(shí)候(如切換虛擬終端或是移動(dòng)鼠標(biāo)光標(biāo)的時(shí)候),制表符還是變成了漢字,因?yàn)橹乩L完全依賴于緩沖區(qū),而這時(shí)用來(lái)記錄字符集狀態(tài)的變量并不反映當(dāng)前字符集狀態(tài)。問(wèn)題還是沒(méi)有最終解決。我們又回到了起點(diǎn)?!? 看來(lái)問(wèn)題的最終解決手段必須是把字符集的狀態(tài)伴隨每一個(gè)字符存在緩沖區(qū)中。讓我們來(lái)研究一下緩沖區(qū)的結(jié)構(gòu)。
每一個(gè)字符占用16bit的緩沖區(qū),低8位是ASCII值,完全被利用,高8位包含前景顏色和背景顏色的屬性,也沒(méi)有多余的空間可以利用。因而只能另外開(kāi)辟新的緩沖區(qū)。為了保持一致性,我們決定在原來(lái)的緩沖區(qū)后面添加相同大小的緩沖區(qū),用來(lái)存放是否是漢字的信息。
也許有讀者會(huì)問(wèn),我們只需要為每個(gè)字符添加一bit的信息來(lái)標(biāo)志是否是漢字就足夠了,為什么還要開(kāi)辟與原緩沖區(qū)大小相同的雙倍緩沖區(qū),是不是太浪費(fèi)呢?
我們先放下這個(gè)問(wèn)題,稍后再作回答。
其實(shí),如果再添加一bit來(lái)標(biāo)志是當(dāng)前字符是漢字的左半邊還是右半邊的話,就會(huì)省去掃描屏幕上當(dāng)前整行字符串的工作,這樣一來(lái),編程會(huì)更簡(jiǎn)單。但是有讀者會(huì)問(wèn),即使是這樣,使用8bit總夠用了吧?為什么還要使用16bit呢?
我們的作法是∶用低8位來(lái)存放漢字另外一半的內(nèi)碼,用高8位中的2 bit來(lái)存放上面所講的輔助信息,高8位的剩余6位可以用來(lái)存放漢字或其它編碼方式(如BIG5或日文、韓文)的信息,從而使我們可以實(shí)現(xiàn)同屏顯示多種雙字節(jié)語(yǔ)言的字符而不會(huì)有相互干擾。另外,在編程時(shí),雙倍緩沖也比較容易計(jì)算。
這樣我們就回答了如上的兩個(gè)問(wèn)題。
迄今為止,我們有了一套徹底解決漢字和制表符相互干擾、半個(gè)漢字的刷新、重繪等問(wèn)題的方案。剩下的就是具體編程實(shí)現(xiàn)的問(wèn)題了。
但是,由于Framebuffer的驅(qū)動(dòng)很多,修改每一個(gè)驅(qū)動(dòng)的xxxx_putc()函數(shù)和xxxx_putcs( )函數(shù)會(huì)是一項(xiàng)不小的工作,而且,改動(dòng)驅(qū)動(dòng)程序后,每種驅(qū)動(dòng)的測(cè)試也是很麻煩的,尤其是對(duì)于有硬件加速的顯卡,修改和測(cè)試會(huì)更不容易。
那么,存不存在一種不需要修改顯卡驅(qū)動(dòng)程序的方法呢?
經(jīng)過(guò)一番努力,我們發(fā)現(xiàn),可以在調(diào)用xxxx_putcs( )或xxxx_putc()函數(shù)輸出漢字之前,修改vga字庫(kù)的指針使其指向所需顯示的漢字在漢字字庫(kù)中的位置,即把一個(gè)漢字當(dāng)成兩個(gè)vga ASCII字符輸出。也就是說(shuō),在內(nèi)核中存在兩個(gè)字庫(kù),一個(gè)是原有的vga字符字庫(kù),另一個(gè)是漢字字庫(kù),當(dāng)我們需要輸出漢字的時(shí)候,就把vga字庫(kù)的指針指向漢字字庫(kù)的相應(yīng)位置,漢字輸出完之后,再把該指針指向vga字庫(kù)的原有位置。
這樣一來(lái),我們只需要修改fbcon.c和console.c,其中console.c負(fù)責(zé)維護(hù)雙倍緩沖區(qū),把每一個(gè)字符的信息存入附加的緩沖區(qū);而fbcon.c負(fù)責(zé)利用雙倍緩沖區(qū)中附加的信息,調(diào)整vga字庫(kù)的指針,調(diào)用底層的顯示驅(qū)動(dòng)程序。
這里還有幾個(gè)需要注意的地方∶
1. 由于屏幕重繪等原因,調(diào)用底層驅(qū)動(dòng)xxxx_putc( )和xxxx_putcs()的地方有多處。我們作了兩個(gè)函數(shù)分別包裝這兩個(gè)調(diào)用,完成替換字庫(kù)、調(diào)用xxxx_putcs( )或xxxx_putc( )、恢復(fù)字庫(kù)等功能。
2.為了實(shí)現(xiàn)向上滾屏(shift pageup)時(shí)也能看到漢字,我們需要作另外的修改。
Linux在設(shè)計(jì)虛擬終端的時(shí)候,提供了回顧被卷出屏幕以外的信息的功能,這就是用熱鍵來(lái)向上滾屏(shift pageup)。當(dāng)前被使用的虛擬終端擁有一個(gè)公共的緩沖區(qū)(soft back),用來(lái)存放被滾出屏幕以外的信息。當(dāng)切換虛擬終端的時(shí)候,公共緩沖區(qū)的內(nèi)容會(huì)被清除而被新的虛擬終端使用。向上滾屏的時(shí)候,顯示的是公共緩沖區(qū)中的內(nèi)容。因此,如果我們想在向上滾屏的時(shí)候看到漢字,公共緩沖區(qū)也必須加倍,以確保沒(méi)有信息丟失。當(dāng)滾出屏幕的信息向公共緩沖區(qū)填寫(xiě)的時(shí)候,必須把相應(yīng)的附加信息也填寫(xiě)進(jìn)公共緩沖區(qū)的附加區(qū)域。這就要求fbcon.c必須懂得利用公共緩沖區(qū)的附加信息。
當(dāng)然,有另外一種偷懶的方法,那就是不允許用戶向上滾屏,從而避免對(duì)公區(qū)緩沖區(qū)的處理。
3.把不同的編碼方式(GB、BIG5、日文和韓文)寫(xiě)成不同的module,以實(shí)現(xiàn)動(dòng)態(tài)加載,從而使得擴(kuò)展新的編碼方式不需要重新編譯核心。
小結(jié)
通過(guò)這次針對(duì)Linux核心的探索,我們發(fā)現(xiàn),目前Linux的核心設(shè)計(jì)中,完全沒(méi)有考慮到雙字節(jié)編碼字符的顯示。我們?cè)谶@種情況下摸索出一套解決核心下漢字顯示的方法,并編碼實(shí)現(xiàn)了該方案.
遵循核心的GPL版權(quán)聲明,我們同時(shí)公布了實(shí)現(xiàn)這一技術(shù)的源代碼,當(dāng)然,這些改動(dòng)仍然是GPL的.如果能對(duì)研究核心的朋友有所幫助,減少一些大家對(duì)核心的神秘感,將是我們最大的收獲。
但是對(duì)核心和中文化來(lái)說(shuō),這僅僅是一種嘗試,遠(yuǎn)不是終點(diǎn).這種改動(dòng)多少帶有一些hack的色彩,不太可能融合進(jìn)權(quán)威的核心里去.我們?nèi)栽诜e極探索圓滿解決這一問(wèn)題的方法,相信這一結(jié)果必然需要通過(guò)國(guó)內(nèi)外Linux群體的共同努力才能實(shí)現(xiàn).我們也非常歡迎大家和我們共同討論這一問(wèn)題.
測(cè)試
本文實(shí)現(xiàn)的Kernel Patch文件(patch.kernel.chinese)可以從http:/www.turbolinux.com.cn下載。Cd /usr/src/(該目錄下應(yīng)有Linux核心源程序所在的目錄linux/) patch -p0 -b patch.kernel.chinese make menuconfig 請(qǐng)選擇Console drivers選項(xiàng)中的
〔*〕 Double Byte Character Display Support(EXPERIMENTAL)
〔*〕 Double Byte GB encode (module only)
〔*〕 VESA VGA graphics console
*> Virtual Frame Buffer support (ONLY FOR TESTING!)
*> 8 bpp packed pixels support
*> 16 bpp packed pixels support
*> VGA characters/attributes support
〔*〕 Select compiled-in fonts
〔*〕VGA 8x8 font
〔*〕VGA 8x16 font
make dep
make bzImage
make modules
make install
make modules_install
然后用新的核心啟動(dòng)。
Insmod encode-gb.o
linux操作系統(tǒng)文章專題:linux操作系統(tǒng)詳解(linux不再難懂)
評(píng)論