萬盛學電腦網

 萬盛學電腦網 >> Linux教程 >> 基於Linux系統核心的漢字顯示嘗試

基於Linux系統核心的漢字顯示嘗試

class="21408">

  在闡述基於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.n");

}

  puts函數向缺省輸出文件(/dev/tty1)發出寫的系統調用write(2)。系統調用到linux核心裡面對應的核心函數是console.c中的con_write(),con_write()最終會調用do_con_write( )。在do_con_write( )中負責把"hello, world.n"這個字符串放到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.n"中的每一個字符都要經過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.n"中的字符被一個一個地填寫到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(你好n );(你好的內碼為0xc4,0xe3,0xba,0xc3)。這時候會怎麼樣呢,有一點可以肯定,"你好"肯定不會出現在屏幕上,原因有∶核心中沒有漢字字庫,中文顯示就是無米之炊了.

  在負責字符顯示的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裡,有根據字符轉義序列命令來記錄字符狀態的變量。結合該變量提供的信息,就可以非常干淨地把制表符與漢字區別開來。

  在如上思路的指引

copyright © 萬盛學電腦網 all rights reserved