首页|社区论坛|iTePub书库|在线阅读|电图引擎|技术文档|图书查询|光盘刻录 |

返回首页 我的控制台 会员登陆 免费注册 最后更新 高级搜索 我要投稿 退出登陆 联系我们
操作系统
计算机应用技术
解决方案
精彩书评
开发语言
认证考试
软件工程
数据库
算法与数据结构
网络技术与安全
网站制作
移动通信
电子图书籍下载:会员文档首页 / 操作系统 / linux / Linux深入技术 / 基于Linux核心的汉字显示的尝试
基于Linux核心的汉字显示的尝试
2004-11-01    利启诚/孙喜明       点击: 1479
基于Linux核心的汉字显示的尝试
原理

在阐述“基于Linux核心的汉字显示”的技术细节之前,有必要介绍一下原有linux
的工作机制。这里主要涉及到两部分的知识,就是Linux下终端和帧缓冲的实现.
控制台(console)
通常我们在linux下看到的控制台(console)是由几个设备完成的。分别是/dev/tty
N(其中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/目录下。总的抽象设备文件为f
bcon.c,在这个目录下还有与各种显卡驱动相关的源文件。
在使用帧缓冲时,Linux是将显卡置于图形模式下的.
试验

我们以一个简单的例子来说明字符显示的过程。我们假设是在虚拟终端1(/dev/tt
y1)下运行一个如下的简单程序。
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;//我们用到了d
river_data变量
. . . . . .
currcons = vt->vc_num; //我们在这里的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)。这时候会怎么样呢,有一点可以肯定,"你好"肯定不会出现在屏幕
上,原因有∶
 核心中没有汉字字库,中文显示就是无米之炊了.
1 在负责字符显示的void fbcon_cfb8_putcs( )函数中,原有操作如下∶
对于每个要显示的字符,依次从虚拟终端缓冲区中以WORD为单位读取(低位字节是A
SCII码,高8位是字符的属性),由于汉字是双字节编码方式,所以这种操作是不可
能显示出汉字的,只能显示出xxxx_putcs()是一个一个VGA字符.
要解决的问题∶
确保在do_con_write( )时uni□pc转换不会改变原有编码。一个很直接的实现方式就
是加载一个我们自己定制的UNICODE映射表,loadunimap direct.uni,或者直接把d
irect.uni置为核心的缺省映射表。
针对如上问题,我们要做的第一个尝试方案是如下。
首先需要在核心中加载汉字字库,然后修改fbcon_cfb8_putcs( )函数,在fbcon_cf
b8_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_p
utcs( )函数会是一项不小的工作,而且,改动驱动程序后,每种驱动的测试也是很
麻烦的,尤其是对于有硬件加速的显卡,修改和测试会更不容易。
那么,存不存在一种不需要修改显卡驱动程序的方法呢?
经过一番努力,我们发现,可以在调用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)可以从http://www.turbol/
inux.com.cn下载。
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

Document converted from word 8 by MSWordView (mswordview
0.5.2)

责任编辑: sky_walker
发表评论 查看评论 加入收藏 Email给朋友 打印本文
如果你想对该文章评分, 请先登陆, 如果你仍未注册,请点击注册链接注册成为本站会员.
平均得分 0, 共 0 人评分
1 2 3 4 5 6 7 8 9 10

  Copyright © 2002 - 2005 iTePub.Group All Rights Reserved