Linux 字符编码支持

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 Linux 4.14 内核源码 进行分析。
另外,阅读本文需要一些字符编码的前置知识,有需要的读者可参考:
字符编码
什么是UTF-8
来了解 字符编码 的基础知识。

3. 为什么需要更多的字符集编码?

在开发过程中,常常需要为产品的不同销售区域,提供各种语种支持,别的不说,作为国人,产品本土化的中文支持也是最起码的要求,此时 ASCII 码 已经不能够满足需求,所以需要其它的字符编码的支持。如果是在 Windows 下,这是一个相对简单的工作;如果是 PC 平台的 Linux 系统,这些工作可能也不会很困难;但如果是在嵌入式 Linux 环境,想要支持不同语种字符集,那恐怕得有一番折腾。

4. 提供更多字符集编码支持需要做哪些工作?

先思考下,在 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, ....

5. Linux 是怎样提供各种字符集支持的?

Linux 对多种字符集的支持,我将其分为 内核空间文件系统用户空间 两部分来讲述。本文以 GB2312 字符集支持为例来进行说明,其它语种字符集编码的情形类似。

5.1 文件系统各种字符集支持

5.1.1 注册可支持的语种字符集

内核通过数据结构 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;	
}

5.1.2 文件系统使用系统中注册的字符集

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/

5.2 用户空间各种字符集支持

用户空间的字符集支持主要围绕 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/
如果在代码中需支持非 ASCII 编码的字符串操作,最好将这些字串通过工具(如 gettext)转换成字节数组进行编译。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值