限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
本文基于 Linux 4.14 内核源码
进行分析。
另外,阅读本文需要一些字符编码
的前置知识,有需要的读者可参考:
字符编码
什么是UTF-8
来了解 字符编码
的基础知识。
在开发过程中,常常需要为产品的不同销售区域,提供各种语种支持
,别的不说,作为国人,产品本土化的中文支持也是最起码的要求,此时 ASCII 码
已经不能够满足需求,所以需要其它的字符编码的支持。如果是在 Windows
下,这相对是一个简单的工作;如果是 PC 平台的 Linux 系统
,这些工作可能也不会很困难;但如果是在嵌入式 Linux 环境
,想要支持不同语种字符集,那恐怕得有一番折腾。
先思考下,在 Linux
下如果要提供 ASCII 码
外的更多字符集编码支持,有哪些工作需要做?字符集相关的处理,无非就是 内存字符串操作
和 文件名存储路径字串
,对于 文件内容的不同字符集编码
,可以一律视为二进制数据
,具体内容的编解码留给应用去完成
。下面列出了在支持不同字符集编码时,可能需要系统所做工作的清单:
可能要做的工作 | 可能需要的原因
-----------------------------------|----------------------------------------------------------
glibc适配字符串函数(strlen()等) | 如 UNICODE 字符,因为编码包含 '\0', strlen() 等函数就没法
| 正常工作。
-----------------------------------|----------------------------------------------------------
glibc字符串打印函数适配(printf()等) | 对于 UNICODE 字符集,如果还是按单个字节来打印,必定会出现一
| 些乱码字符,因为按 0~255 范围去索引字体模型点阵数据。不仅要
| 重新适配 printf() 等打印接口,还要提供 UNICODE 字符的字体。
-----------------------------------|----------------------------------------------------------
编译器支持 | 很不幸,如果代码里面包含了 UNICODE 字符(如包含 UNICODE 字符
| 串),而且文件存储为 UNICODE 格式,那编 GCC 译器是无法成功
| 编译的,这时候需要将文件存储为 ASCII 文本模式。
-----------------------------------|----------------------------------------------------------
内核空间文件系统适配 | 如用 UNICODE 来编码文件名创建新文件时,调用 VFS 的 sys_open()
| 系列接口,以及 VFS 最终调用的实际文件系统(如 vfat_create())
| 的创建接口,以及文件存储路径,VFS 以及实际的文件系统都需要提供
| UNICODE 支持。
上述 VFS
指 Linux 虚拟文件系统,从文件系统的框架来看,是位于 实际文件系统
和 文件系统用户空间接口
之间的中间层,这3层的层次关系如下图所示:
用户空间 open()/close/ioctl()/...
^
|
---------------------------|------------------
V
内核空间 VFS(sys_open()/sys_close()/sys_ioctl())
^
|
V
vfat, ext2, ext3, ext4, ubifs, ....
Linux 对多种字符集的支持,我将其分为 内核空间文件系统
和 用户空间
两部分来讲述。本文以 GB2312
字符集支持为例来进行说明,其它语种字符集编码的情形类似。
内核通过数据结构 struct nls_table
和 接口函数 register_nls()
来提供多种语种字符集的支持。看一下 GB2312
字符集的情况:
/* fs/nls/Makefile */
obj-$(CONFIG_NLS_CODEPAGE_936) += nls_cp936.o # 首先要开启 CONFIG_NLS_CODEPAGE_936 配置项
/* fs/nls/nls_cp936.c */
static struct nls_table table = {
.charset = "cp936",
.alias = "gb2312", // "gb2312" 是 "cp936" 的别名
.uni2char = uni2char,
.char2uni = char2uni,
.charset2lower = charset2lower,
.charset2upper = charset2upper,
};
static int __init init_nls_cp936(void)
{
return register_nls(&table);
}
static void __exit exit_nls_cp936(void)
{
unregister_nls(&table);
}
module_init(init_nls_cp936)
module_exit(exit_nls_cp936)
/* include/linux/nls.h */
/* Plane-0 Unicode character */
typedef u16 wchar_t;
#define MAX_WCHAR_T 0xffff
/* Arbitrary Unicode character */
typedef u32 unicode_t;
struct nls_table {
const char *charset;
const char *alias;
/*
* fs/nls/nls_cp936.c: uni2char(), char2uni()
* fs/nls/nls_utf8.c: uni2char(), char2uni()
* ...
*/
int (*uni2char) (wchar_t uni, unsigned char *out, int boundlen);
int (*char2uni) (const unsigned char *rawstring, int boundlen,
wchar_t *uni);
const unsigned char *charset2lower;
const unsigned char *charset2upper;
struct module *owner;
struct nls_table *next;
};
...
#define register_nls(nls) __register_nls((nls), THIS_MODULE)
/* fs/nls/nls_base.c */
static struct nls_table default_table;
static struct nls_table *tables = &default_table;
static DEFINE_SPINLOCK(nls_lock);
int __register_nls(struct nls_table *nls, struct module *owner)
{
struct nls_table ** tmp = &tables;
if (nls->next)
return -EBUSY;
nls->owner = owner;
spin_lock(&nls_lock);
while (*tmp) {
if (nls == *tmp) {
spin_unlock(&nls_lock);
return -EBUSY;
}
tmp = &(*tmp)->next;
}
nls->next = tables;
tables = nls;
spin_unlock(&nls_lock);
return 0;
}
以 FAT
文件系统为例,来看它是如何支持 GB2312
的。在挂载 FAT
文件系统的时候,可以指定其支持的语种字符集:
mount -t vfat -o iocharset=gb2312,codepage=936 /dev/XXX /mnt/YYY
顺便解释下,这里为什么是 -t vfat
而不是 -t fat
?因为在 Linux 内核里面,为 FAT
注册了 "vfat"
和 "msdos"
两个文件系统,它们彼此并不相同。进一步看一下 vfat
的挂载过程,重点是看其 -o
选项的解析过程:
sys_mount() /* fs/namespace.c */
do_mount()
do_new_mount()
vfs_kern_mount()
mount_fs(type, flags, name, data) /* fs/super.c */
type->mount(type, flags, name, data) = vfat_mount(type, flags, name, data)
vfat_mount() /* fs/fat/namei_vfat.c */
mount_bdev(fs_type, flags, dev_name, data, vfat_fill_super)
fill_super(s, data, flags & SB_SILENT ? 1 : 0) = vfat_fill_super()
fat_fill_super(sb, data, silent, 1, setup)
/* fs/fat/inode */
int fat_fill_super(struct super_block *sb, void *data, int silent, int isvfat,
void (*setup)(struct super_block *))
{
...
sb->s_op = &fat_sops; /* 设置 super block 接口 */
...
/* 解析 -o iocharset=gb2312,codepage=936 选项 */
error = parse_options(sb, data, isvfat, silent, &debug, &sbi->options);
...
/* 设置 vfat 文件系统接口 */
/*
* fs/fat/namei_vfat.c:
* static const struct inode_operations vfat_dir_inode_operations = {
* .create = vfat_create,
* .lookup = vfat_lookup,
* .unlink = vfat_unlink,
* .mkdir = vfat_mkdir,
* .rmdir = vfat_rmdir,
* .rename = vfat_rename,
* .setattr = fat_setattr,
* .getattr = fat_getattr,
* };
*/
setup(sb); /* flavour-specific stuff that needs options */
MSDOS_SB(sb)->dir_ops = &vfat_dir_inode_operations;
if (MSDOS_SB(sb)->options.name_check != 's')
sb->s_d_op = &vfat_ci_dentry_ops;
else
sb->s_d_op = &vfat_dentry_ops;
...
/*
* 根据 -o iocharset=gb2312,codepage=936 设置语种字符集支持接口
*/
sprintf(buf, "cp%d", sbi->options.codepage);
sbi->nls_disk = load_nls(buf);
if (sbi->options.isvfat) {
sbi->nls_io = load_nls(sbi->options.iocharset);
...
}
...
}
/* 解析 -o iocharset=gb2312,codepage=936 选项 */
static int parse_options(struct super_block *sb, char *options, int is_vfat,
int silent, int *debug, struct fat_mount_options *opts)
{
opts->isvfat = is_vfat;
...
opts->codepage = fat_default_codepage;
...
opts->utf8 = IS_ENABLED(CONFIG_FAT_DEFAULT_UTF8) && is_vfat;
...
while ((p = strsep(&options, ",")) != NULL) {
....
token = match_token(p, fat_tokens, args);
if (token == Opt_err) {
if (is_vfat)
token = match_token(p, vfat_tokens, args);
else
token = match_token(p, msdos_tokens, args);
}
switch (token) {
...
case Opt_codepage: /* codepage=936 */
if (match_int(&args[0], &option))
return -EINVAL;
opts->codepage = option; // opts->codepage = 936;
break;
...
case Opt_charset: /* iocharset=gb2312 */
fat_reset_iocharset(opts);
iocharset = match_strdup(&args[0]);
opts->iocharset = iocharset; // opts->iocharset = "gb2312";
break;
...
}
}
...
return 0;
}
/*
* 加载文件系统支持的语种字符集:
* sbi->nls_disk = load_nls(buf);
* sbi->nls_io = load_nls(sbi->options.iocharset);
*/
struct nls_table *load_nls(char *charset)
{
return try_then_request_module(find_nls(charset), "nls_%s", charset);
}
/* 按 @charset ("cp936", "gb2312") 查找 register_nls() 注册的语种字符集对象 */
static struct nls_table *find_nls(char *charset)
{
struct nls_table *nls;
spin_lock(&nls_lock);
for (nls = tables; nls; nls = nls->next) {
if (!strcmp(nls->charset, charset))
break;
if (nls->alias && !strcmp(nls->alias, charset))
break;
}
if (nls && !try_module_get(nls->owner))
nls = NULL;
spin_unlock(&nls_lock);
return nls;
之后在创建 文件路径在文件系统内部记录数据
等操作中(如 open() 创建文件,mkdir() 创建目录
),可以看到文件系统字符集转码的工作。 以 open() 调用新建文件
为例,我们来看 FAT
文件系统是如何提供 GB2312
字符集支持的:
open("中文命名文件.txt", O_CREAT | O_RDWR)
...
vfat_create() /* fs/fat/namei */
...
err = vfat_add_entry(dir, &dentry->d_name, 0, 0, &ts, &sinfo);
err = vfat_build_slots(dir, qname->name, len, is_dir, cluster, ts,
slots, &nr_slots);
/*
* VFS 目前始终按 ASCII 码进行识别,实际的字符集转换落到了 vfat:
* iocharset=gb2312
*/
err = xlate_to_uni(name, len, (unsigned char *)uname, &ulen, &usize,
opts->unicode_xlate, opts->utf8, sbi->nls_io);
if (utf8) {
} else {
for (i = 0, ip = name, op = outname, *outlen = 0;
i < len && *outlen < FAT_LFN_LEN;
*outlen += 1) {
if (...) {
} else {
/* fs/nls/nls_cp936.c: char2uni() */
charlen = nls->char2uni(ip, len - i,
(wchar_t *)op);
}
}
}
...
/*
* VFS 目前始终按 ASCII 码进行识别,实际的字符集转换落到了 vfat:
* codepage=936
*/
err = vfat_create_shortname(dir, sbi->nls_disk, uname, ulen,
msdos_name, &lcase);
...
for (baselen = i = 0, p = base, ip = uname; i < sz; i++, ip++) {
chl = to_shortname_char(nls, charbuf, sizeof(charbuf),
ip, &base_info);
...
/* fs/nls/nls_cp936.c: uni2char() */
len = nls->uni2char(*src, buf, buf_size);
...
}
...
...
mkdir()
的情形类似,在此不再赘述。从上面的分析可以了解到,VFS
对这些编码是没有感知的,始终是以 ASCII
字符编码在进行操作,对于 UTF-8, GB2312
这些不会出现 \0
(除 \0
本身外)的字符集来说,这没有问题,但如果直接传递给 VFS
一个 UNICODE
字串(不经应用层转换为其它字符集),立马就凉凉了。不过 Linux 5.x
已经内置了 UNICODE
的支持,更多细节可参考此处: https://git.kernel.org/pub/scm/linux/kernel/git/krisman/unicode.git/ 。
用户空间的字符集支持主要围绕 setlocale()
函数 和 locale
工具进行。本文不做展开,感兴趣的读者可参考如下几篇博客:
https://www.cnblogs.com/h2zZhou/p/5324385.html
https://www.cnblogs.com/lizm166/p/12598731.html
https://wiki.archlinux.org/title/Localization/Simplified_Chinese
https://www.linux.com/news/using-unicode-linux/
由于 GCC
没法直接编译除 ASCII 文本
之外的其它格式的文件,所以如果在代码中需支持非 ASCII
编码的字符串操作,最好将这些字串通过工具(如 gettext)转换成字节数组进行编译。