基於Linux核心的漢字顯示的嘗試(轉)

ba發表於2007-08-12
基於Linux核心的漢字顯示的嘗試(轉)[@more@]在闡述基於Linux核心的漢字顯示的技術細節之前,有必要介紹一下原有linux的工作機制。這裡主要涉及到兩部分的知識,就是Linux下終端和幀緩衝的實現.

控制檯(console)

通常我們在linux下看到的控制檯(console)是由幾個裝置完成的。分別是/dev/ttyN(其中tty0就是/dev/console,tty1,tty2就是不同的虛擬終端(virtual console)).通常使用熱鍵alt+Fn來在這些虛擬終端之間進行切換。所有的這些tty裝置都是由linux/drivers/char/console.c和vt.c對應。其中console.c負責繪製螢幕上的字元,vt.c負責管理不同的虛擬終端,並且負責提供console.c需要繪製的內容。Vt.c把不同虛擬終端下需要交給console.c繪製的內容放到不同的快取中去。Vt.c管理著這樣一個緩衝區的陣列,並且負責在其間切換,以指定哪一個緩衝區是被啟用的。你所看到的虛擬終端就對應著被啟用的緩衝區。Console.c同時也負責接收終端的輸入,然後把接收到的輸入放到緩衝區。

幀緩衝(framebuffer)

Framebuffer是把視訊記憶體抽象後的一種裝置,可以透過這個裝置的讀寫直接對視訊記憶體進行操作。這種操作是抽象的,統一的。使用者不必關心物理視訊記憶體的位置、換頁機制等等具體細節。這些都是由Framebuffer裝置驅動來完成的。

Framebuffer對應的原始檔在linux/drivers/video/目錄下。總的抽象裝置檔案為fbcon.c,在這個目錄下還有與各種顯示卡驅動相關的原始檔。在使用幀緩衝時,Linux是將顯示卡置於圖形模式下的.

試驗

我們以一個簡單的例子來說明字元顯示的過程。我們假設是在虛擬終端1(/dev/tty1)下執行一個如下的簡單程式。

main ( )

{

puts("hello, world. ");

}

puts函式向預設輸出檔案(/dev/tty1)發出寫的系統呼叫write(2)。系統呼叫到linux核心裡面對應的核心函式是console.c中的con_write(),con_write()最終會呼叫do_con_write( )。在do_con_write( )中負責把"hello, world. "這個字串放到tty1對應的緩衝區中去。

do_con_write( )還負責處理控制字元和游標的位置。讓我們來看一下do_con_write()這個函式的宣告。

static int do_con_write(struct tty_struct * tty, int

from_user, const unsigned char *buf, int count) 其中tty是指向tty_struct結構的指標,這個結構裡面存放著關於這個tty的所有資訊(請參照linux/include/linux/tty.h)。Tty_struct結構中定義了通用(或高層)tty的屬性(例如寬度和高度等)。

在do_con_write( )函式中用到了tty_struct結構中的driver_data變數。

driver_data是一個vt_struct指標。在vt_struct結構中包含這個tty的序列號(我們正使用tty1,所以這個序號為1)。Vt_struct結構中有一個vc結構的陣列vc_cons,這個陣列就是各虛擬終端的私有資料。

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://我們在這裡的vc_nums就是1

. . . . .

}

要訪問虛擬終端的私有資料,需使用vc_cons〔currcons〕.d指標。這個指標指向的結構含有當前虛擬終端上游標的位置、緩衝區的起始地址、緩衝區大小等等。

"hello, world. "中的每一個字元都要經過conv_uni_to_pc( )

這個函式轉換成8位的顯示字元。這要做的主要目的是使不同語言的國家能把16位的UniCode碼對映到8位的顯示字符集上,目前還是主要針對歐洲國家的語言,對映結果為8位,不包含對雙位元組(double byte)的範圍。

這種UNICODE到顯示字元的對映關係可以由使用者自行定義。在預設的對映表上,會把中文的字元對映到其他的字元上,這是我們不希望看到也是不需要的。所以我們有兩個選擇∶

1不進行conv_uni_to_pc( )的轉換。

2載入符合雙位元組處理的對映關係,即對非控制字元進行1對1的不變對映。我們自己定製的符合這種對映關係的UNICODE碼錶是direct.uni。

要想檢視/裝載當前系統的unicode對映表,可使外部命令loadunimap。

經過conv_uni_to_pc( )轉換之後,"hello, world. "中的字元被一個一個地填寫到tty1的緩衝區中。然後do_con_write( )呼叫下層的驅動,把緩衝區中的內容輸出到顯示器上(也就相當於把緩衝區的內容複製到VGA視訊記憶體中去)。

sw->con_putcs(vc_cons〔currcons〕.d, (u16 *)draw_from, (u16

*)draw_to-(u16 *)draw_from, y, draw_x);

之所以要呼叫底層驅動,是因為存在不同的顯示裝置,其對應VGA視訊記憶體的存取方式也不一樣。

上面的Sw->con_putcs( )就會呼叫到fbcon.c中的fbcon_putcs()函式(con_putcs是一個函式的指標,在Framebuffer模式下指向fbcon_putcs()函式)。也就是說在do_con_write( )函式中是直接呼叫了fbcon_putcs()函式來進行字元的繪製。比如說在256色模式下,真正負責輸出的函式是void fbcon_cfb8_putcs(struct vc_data *conp, struct display *p,const unsigned short *s, int count, int

yy, int xx)

顯示中文

比如說我們試圖輸出一句中文∶putcs(你好 );(你好的內碼為0xc4,0xe3,0xba,0xc3)。這時候會怎麼樣呢,有一點可以肯定,"你好"肯定不會出現在螢幕上,原因有∶核心中沒有漢字字型檔,中文顯示就是無米之炊了.

1在負責字元顯示的void fbcon_cfb8_putcs( )函式中,原有操作如下∶對於每個要顯示的字元,依次從虛擬終端緩衝區中以WORD為單位讀取(低位位元組是ASCII碼,高8位是字元的屬性),由於漢字是雙位元組編碼方式,所以這種操作是不可能顯示出漢字的,只能顯示出xxxx_putcs()是一個一個VGA字元.

要解決的問題∶

確保在do_con_write( )時uni□pc轉換不會改變原有編碼。一個很直接的實現方式就是載入一個我們自己定製的UNICODE對映表,loadunimapdirect.uni,或者直接把direct.uni置為核心的預設對映表。

針對如上問題,我們要做的第一個嘗試方案是如下。

首先需要在核心中載入漢字字型檔,然後修改fbcon_cfb8_putcs()函式,在fbcon_cfb8_putcs( )中一次讀兩個WORD,檢查這兩個WORD的低位位元組是否能拼成一個漢字,如果發現能拼成一個漢字,就算出這個漢字在漢字字型檔中的偏移,然後把它當成一個16 x 16的VGA字元來顯示。

試驗的結果表明∶

1能夠輸出漢字,但仍有許多不理想的地方,比如說,輸出以半個漢字開始的一串漢字,則這半個漢字後面的漢字都會是亂碼。這是半個漢字的問題。

2游標移動會破壞漢字的顯示。表現為,游標移動過的漢字會變成亂碼。這是因為游標的更新是透過xxxx_putc( )函式來完成的。

xxxx_putc( )函式與xxxx_putcs( )函式實現的功能類似,但是xxxx_putc()函式只重新整理一個字元而不是一個字串,因而xxxx_putc()的輸入引數是一個整數,而不是一個字串的地址。Xxxx_putc( )函式的宣告如下∶void fbcon_cfb8_putc(struct vc_data *conp, struct display *p, int c, int yy, int xx)

下一個嘗試方案就是同時修改xxxx_putcs( )函式和xxxx_putc()函式。為了解決半個漢字的問題,每一次輸出之前,都從螢幕當前行的起始位置開始掃描,以確定要輸出的字元是否落在半個漢字的位置上。如果是半個漢字的位置,則進行相應的調整,即從向前移動一

個位元組的位置開始輸出。

這個方案有一個困難,即xxxx_putc( )函式不用緩衝區的地址,而是用一個整數作為引數。所以xxxx_putc( )無法直接利用相鄰的字元來判別該定符是否是漢字。

解決方案是,利用xxxx_putc( )的游標位置引數(yy, xx),可以逆推出該字元在緩衝區中的位置。但仍有一些小麻煩,在Linux的虛擬終端下,使用者可能會上卷該螢幕(shift + pageup),導致游標的y座標和相應字元在緩衝區的行數不一致。相應的解決方案是,在逆推的過程中,考慮卷屏的參量。

這樣一來,我們就又進了一步,得到了一個相對更好的版本。但仍有問題沒有解決。敲入turbonetcfg,會發現選單的邊框字元也被當成漢字顯示。這是因為,這種邊框字元是擴充套件字元,也使用了字元的第8位,因而被當作漢字來顯示。例如,單線一的製表符內碼為0xC4,當連成一條長線就是由一連串0xC4組成,而0xC4C4正是漢字哪。於是水平的製表符被一連串的哪字替代了。要解決這個問題就非常不容易了,因為製表符的種類比較多,而且垂直製表符與其後面字元的組合型式又多種多樣,因而很難判斷出相應位置的字元是不是製表符,從理論上說,無論採取什麼樣的排除演算法,都必然存在誤判的情況,因為總存在二義性,沒有充足的條件來推斷出當前字元究竟是製表符還是漢字。

我們一方面尋找更好的排除組合演算法,一方面試圖尋找其它的解決方案。要想從根本上解決定個問題,必須利用其它的輔助資訊,僅僅從緩衝區的字元來判斷是不夠的。

經過一番努力,我們發現,在UNIX中使用擴充套件字元時,都要先輸出字元轉義序列(Escape sequence)來切換當前字符集。字元轉義序列是以控制字元Esc為首的控制命令,在UNIX的虛擬終端中完成終端控制命令,這種命令包括,移動游標座標、卷屏、刪除、切換字符集等等。也就是說在輸出代表製表符的字串之前,通常是要先輸出特定的字元轉義序列。在console.c裡,有根據字元轉義序列命令來記錄字元狀態的變數。結合該變數提供的資訊,就可以非常乾淨地把製表符與漢字區別開來。

在如上思路的指引下,我們又產生了新的解決方案。經過改動得到了另一各版本.

在這個新版本上,turbonetcfg在初次繪製的時候,製表符與漢字被清晰地區分開來,結果是非常正確的。但還有新的問題存在∶turbonetcfg在重繪的時候(如切換虛擬終端或是移動滑鼠游標的時候),製表符還是變成了漢字,因為重繪完全依賴於緩衝區,而這時用來記錄字符集狀態的變數並不反映當前字符集狀態。問題還是沒有最終解決。我們又回到了起點。∶( 看來問題的最終解決手段必須是把字符集的狀態伴隨每一個字元存在緩衝區中。讓我們來研究一下緩衝區的結構。

每一個字元佔用16bit的緩衝區,低8位是ASCII值,完全被利用,高8位包含前景顏色和背景顏色的屬性,也沒有多餘的空間可以利用。因而只能另外開闢新的緩衝區。為了保持一致性,我們決定在原來的緩衝區後面新增相同大小的緩衝區,用來存放是否是漢字的資訊。

也許有讀者會問,我們只需要為每個字元新增一bit的資訊來標誌是否是漢字就足夠了,為什麼還要開闢與原緩衝區大小相同的雙倍緩衝區,是不是太浪費呢?

我們先放下這個問題,稍後再作回答。

其實,如果再新增一bit來標誌是當前字元是漢字的左半邊還是右半邊的話,就會省去掃描螢幕上當前整行字串的工作,這樣一來,程式設計會更簡單。但是有讀者會問,即使是這樣,使用8bit總夠用了吧?為什麼還要使用16bit呢?

我們的作法是∶用低8位來存放漢字另外一半的內碼,用高8位中的2 bit來存放上面所講的輔助資訊,高8位的剩餘6位可以用來存放漢字或其它編碼方式(如BIG5或日文、韓文)的資訊,從而使我們可以實現同屏顯示多種雙位元組語言的字元而不會有相互干擾。另外,在程式設計時,雙倍緩衝也比較容易計算。

這樣我們就回答瞭如上的兩個問題。

迄今為止,我們有了一套徹底解決漢字和製表符相互干擾、半個漢字的重新整理、重繪等問題的方案。剩下的就是具體程式設計實現的問題了。

但是,由於Framebuffer的驅動很多,修改每一個驅動的xxxx_putc()函式和xxxx_putcs( )函式會是一項不小的工作,而且,改動驅動程式後,每種驅動的測試也是很麻煩的,尤其是對於有硬體加速的顯示卡,修改和測試會更不容易。

那麼,存不存在一種不需要修改顯示卡驅動程式的方法呢?

經過一番努力,我們發現,可以在呼叫xxxx_putcs( )或xxxx_putc()函式輸出漢字之前,修改vga字型檔的指標使其指向所需顯示的漢字在漢字字型檔中的位置,即把一個漢字當成兩個vga ASCII字元輸出。也就是說,在核心中存在兩個字型檔,一個是原有的vga字元字型檔,另一個是漢字字型檔,當我們需要輸出漢字的時候,就把vga字型檔的指標指向漢字字型檔的相應位置,漢字輸出完之後,再把該指標指向vga字型檔的原有位置。

這樣一來,我們只需要修改fbcon.c和console.c,其中console.c負責維護雙倍緩衝區,把每一個字元的資訊存入附加的緩衝區;而fbcon.c負責利用雙倍緩衝區中附加的資訊,調整vga字型檔的指標,呼叫底層的顯示驅動程式。

這裡還有幾個需要注意的地方∶

1. 由於螢幕重繪等原因,呼叫底層驅動xxxx_putc( )和xxxx_putcs()的地方有多處。我們作了兩個函式分別包裝這兩個呼叫,完成替換字型檔、呼叫xxxx_putcs( )或xxxx_putc( )、恢復字型檔等功能。

2.為了實現向上滾屏(shift + pageup)時也能看到漢字,我們需要作另外的修改。

Linux在設計虛擬終端的時候,提供了回顧被卷出螢幕以外的資訊的功能,這就是用熱鍵來向上滾屏(shift + pageup)。當前被使用的虛擬終端擁有一個公共的緩衝區(soft back),用來存放被滾出螢幕以外的資訊。當切換虛擬終端的時候,公共緩衝區的內容會被清除而被新的虛擬終端使用。向上滾屏的時候,顯示的是公共緩衝區中的內容。因此,如果我們想在向上滾屏的時候看到漢字,公共緩衝區也必須加倍,以確保沒有資訊丟失。當滾出螢幕的資訊向公共緩衝區填寫的時候,必須把相應的附加資訊也填寫進公共緩衝區的附加區域。這就要求fbcon.c必須懂得利用公共緩衝區的附加資訊。

當然,有另外一種偷懶的方法,那就是不允許使用者向上滾屏,從而避免對公區緩衝區的處理。

3.把不同的編碼方式(GB、BIG5、日文和韓文)寫成不同的module,以實現動態載入,從而使得擴充套件新的編碼方式不需要重新編譯核心。

小結

透過這次針對Linux核心的探索,我們發現,目前Linux的核心設計中,完全沒有考慮到雙位元組編碼字元的顯示。我們在這種情況下摸索出一套解決核心下漢字顯示的方法,並編碼實現了該方案.

遵循核心的GPL版權宣告,我們同時公佈了實現這一技術的原始碼,當然,這些改動仍然是GPL的.如果能對研究核心的朋友有所幫助,減少一些大家對核心的神秘感,將是我們最大的收穫。

但是對核心和中文化來說,這僅僅是一種嘗試,遠不是終點.這種改動多少帶有一些hack的色彩,不太可能融合進權威的核心裡去.我們仍在積極探索圓滿解決這一問題的方法,相信這一結果必然需要透過國內外Linux群體的共同努力才能實現.我們也非常歡迎大家和我們共同討論這一問題.

測試

本文實現的Kernel Patch檔案(patch.kernel.chinese)可以從下載。Cd /usr/src/(該目錄下應有Linux核心源程式所在的目錄linux/) patch -p0 -b < patch.kernel.chinese make menuconfig 請選擇Console drivers選項中的

〔*〕 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

然後用新的核心啟動。

Insmod encode-gb.o

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-952392/,如需轉載,請註明出處,否則將追究法律責任。

相關文章