歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> Linux文化 >> 基於Linux操作系統核心的漢字顯示

基於Linux操作系統核心的漢字顯示

日期:2017/2/27 11:54:39   编辑:Linux文化

在闡述“基於Linux核心的漢字顯示”的技術細節之前,有必要介紹一下原有Linux的工作機制。這裡主要涉及到兩部分的知識,這是Linux下終端和幀緩沖的實現。

控制台(console)

通常我們在Linux下看到的控制台(console)是由幾個設備構成的。分別是/dev/ttyN(其中tty0就是/dev/console, tty1、tty2就是不同的虛擬終端(virtual console))。通常使用熱鍵Alt+Fn來在這些虛擬終端之間進行切換。這些tty設備對應於 linux/drivers/char/console.c和lvt.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.

”);

}

pputs函數向缺省輸出文件(/dev/tty)發出“寫”的系統調用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_srtuct結構中定義了?p> 用(或高層)tty的屬性(例如寬度和高度等)。

在do_con_write()函數中用到了tty_struct結構中的driver_data變量。Driver_data是一個 vt_vt_stuct指針。在vt_struct結構中包含這個tty的序列號(我們正使用tty1,所以這個序號為1)。Vt_struct結構中有一個vc結構的數組vc_cons,這個數組就是各虛擬終端的私有數據。

Static int do_write(struct

Tty_struct * tty, int

From_user,const unsigned char

*buf, int conut)

{

struct vt_struct *vt = (struct

vt_struct *)tty_>driver_data;

//我們用到了driver_data變量

…………

currcons = vt->_num;

//在這裡的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) 加載符合雙字節處理的映射關系,即對蜚 控制字符進行一對一的不變映射,我們自己定制的符合這種映射關系的Unicode碼表是direct.uni。要想看/裝載當前系統的Unicode映射表,可使用外部命令loadunimap。 經過conv_uni_to_pc()轉換之後,”hello, world.”中的字符被一個一個地填寫到tty的緩沖區中,然後do_con_write()調用底層的驅動程序,把緩沖區中的內容輸出到顯示器上(也就相當於把緩沖區的內容拷貝到VGA顯存中去):

sw->con putcs(vc_cons[currcons].d,

(u16 *)draw_from, (u16 *)draw_to_

(u16 *)draw_rwom, 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_d

ta *conp,struct display *p, const unsignde short *s, int count, int YY, int xx ) 顯示中文

比如說我們試輸出一句中文:putcs(你好 ”)(“你好”的內碼為0xc4.0xe3.0ba.0xc3)。這時候會怎麼樣呢?有一點可以肯定,“你好”肯定不會出現在屏幕上,原因是:

1、核心中沒有漢字字庫,中文顯示就是無米之炊了。

2、在負責字符顯示的void fbcon_cfb8_putcs()函數中,原有操作如下:

對於每個要顯示的字符,依次從虛擬終端緩沖區中以WORD為單位讀取(低位字節是ASCII碼,高8位是字符的屬性)。由於漢字是雙字節編碼方式,所以這種操作是不可能顯示出漢字的,只能顯示xxxx_putcs()輸出的是一個一個的VGA字符。

因此,要解決的問題:確保在調用do_con_write()時進行uni_pc轉換不會改變原有編碼,一個很直接的實現方式就是加載一個我們自己定制的 Unicode映射表,loadunimap dirdct.uni,或者進接把direct.uni設置為核心的缺省映射表。

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

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

試驗的結果表明:

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

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

xxxx_putc()函數與xxxx_putcs()函數實現的功能夠類似,但是xxxx_()函數只刷新一個字符而不是一個字符串,因而xxxx_putc()的輸入參數是一個整數,而不是一個字符串的地址,xxxx_putc()函數的聲明如下:

void fbcon_cfb8_putc(struct vc_data *conp, struct display *p, int c, int YY, int xx)

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

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

解決方案是,利用xxxx_putc()的光標們置參數(yy,xx),可以逆推出該字符在緩沖區中的位置,但仍一些小麻煩,在Linux的虛擬終端下,用戶可能會上卷該屏幕(Shift+Pageup),導致光標的y座標和相應字符在緩沖區的行數不一致,相應的解決方案是,在逆推的過程中,考慮在屏的參量。

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

我們一方面尋找更好的排除組合算法,一方面試圖尋找其他的解決方案,要想從根本上解決這個問題,必須利用其他的輔助信息,僅僅利用緩沖區的字符來判斷是不夠的。

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

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

在這個新的版本上,turbonetcfg在初次繪制的時候,制表符與漢字被清晰地區分開,但還有問題:turbonetcfg在重繪的時候(如切換虛擬終端或是移動鼠標光標的),制表符還是變成了漢字,因為重繪完全領帶於緩沖區,而這時用來記錄字符集狀態的變量並不反映當前字符集狀態。問題還是沒有最終解決,我們又回到了起點。

看來問題的最終解決手段必須是把字符集的狀態伴隨著每一個字符在每一個字符占用16位的緩沖區,低6、8位是ASCII值,完全被利用,高8位飲食前量顏色和背景顏色的屬性,也沒有多余的空間可以利用,因而只能另外開辟新的緩沖區。為了保持一致性,我們決定在原來的緩沖區後面添加相同大小的緩沖區,用來存放是否漢字的信息。 也許有讀者會問,只需要為每個字符添加一位信息來標志是否是漢字就足夠了,為什麼還要開辟與原緩沖區大小相同的雙倍緩沖區,這是不是太浪費呢?

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

其實,如果再添加一位來標志當前字符是漢字的左半邊還是歷半邊的話,就會省去掃描屏幕上當前整行字符串的工作,這樣一來,編程會更簡單,但是有讀者會問,即使是這樣,使用8位總夠用了吧?為什麼還要使用16位呢?

我們的做法是:用低8位來存放漢字另外一半的內碼,用高8位中的2位來存放上面所講的輔助信息,高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_putc()的地方有多處,我們做了兩個函數分別馐這兩上調用,完成替換字庫、調用xxxx_putcs()或xxxx_putc()、恢復字庫等功能。

2、為了實現向上滾屏時也能看到漢字,我們需要作另外的修改。Linux在設計虛擬終端的時候,提供了回顧被滾出屏幕以外的信息的功能,這就是用熱鍵來向上滾屏(Shift+Pageup)。當前被使用的虎虛擬終端的時候,公共緩沖區的內容會被清除而被新的虛擬終端使用,向上滾屏的時候,顯示的是公共緩沖區中的內容。因此,如果我們想在向上滾屏的時候看到漢字,則公共緩沖區也必須加倍,以確保沒有信息丟失。當滾出屏幕的住處向公共緩沖區填寫的時候,必須把盯應的附加信息也填寫進公共緩沖區的附加區域中,這就要求fbcon.c必須懂得利用公共緩沖區的附加信息。當然,另外有一處偷懶的方法,那就是不允許用戶向上滾屏,從而避免對公區緩沖區的處理。

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

小結

通過這次針對inux核心的探索,我們發現,目前Linux的核心設計中,完全沒有考慮到雙字節編碼字符的顯示,我們在這種情況下摸索出一套解決核心漢字顯示的方法,並編碼實現了該方案。遵循核心的GPL版權聲明,我們同時公布了實現這一技術的源代碼,當然,這些改動仍然是GPL的。如果能對研究核心的朋友有所幫助,養活一些大家對核心的神秘感,將是我們最大的收獲。

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


Copyright © Linux教程網 All Rights Reserved