字符集和编码II: fat/msdos/vfat (文件名乱码的问题)

具体到文件名乱码的问题,需要明确两点

  1. 第一,文件名作为一个字符串,需要被编码后存入文件系统;
  2. 第二,Linux内核无非是个特殊的应用程序,它读取文件名,再把文件名以编码后的形式传递出去。

但Linux内核只能逐字节处理编码流(而Windows NT内核是UCS-2的,逐2字节处理编码流),因此必须采用某种单字节编码(这包括所有的不定长编码)进行输出——这就是Linux内核所谓的NLS,对应于Windows的codepage。

在对文件名的处理上,fat和vfat的区别在于:fat/msdos只支持短文件名(8.3命名法),而vfat加入了对长文件名和UNICODE的支持。

为了保持与fat的兼容性,在vfat中,每个文件同时拥有“长”文件名和短文件名,其中短文件名不区分大小写(实际上是不允许小写字母出现在文件 名中)。可以这么理解,对于vfat,“长文件名”是文件真正的名字,“短文件名”则是提供兼容性的名字。举例来说,文件“真名”为abc.txt,它的 短文件名是ABC.TXT;文件“真名”为alongname.txt,它的短文件名则是ALONGN~1.TXT。

无论是fat还是vfat,短文件名按codepage编码存储,长文件名按UNICODE编码存储。因此,如果文件的真名(也就是长文件名)是s,短文件名是s,则它们在文件系统中分别被存储为

s,φenc(s).

为了访问fat/vfat文件系统,我们需要用内核的msdos或vfat模块。它们有三个跟字符集有关的内核参数:codepage,iocharset,utf8。我们来确定它们对应着什么样的编码或解码函数。

  • codepage=enc:用来转换短文件名中字节码为128~255的代码页。这一选项指定的是短文件名的解码函数φ1enc(因为短文件名被codepage编码了),解码的结果是短文件名被用unicode表示;
  • iocharset=enc:用来转换长文件名和解码后的短文件名。也就是说,这一选项指定编码函数φenc,使得内核最终的输出是经过φenc编码的;
  • utf8:几乎等于iocharset=utf8

这样,参数有如下的具体选择:

  • codepage:这一选项只跟短文件名有关。短文件名可以通过mount -t msdos看到,也可以用windows命令行看到。不在乎短文件名的同学,这个选项完全无所谓;在乎短文件名并且使用简体中文的同学,请写
    codepage=936
  • iocharset:分情况讨论。
    • 若locale不是utf8,则选择相应的locale,比如GB系就应该选
      iocharset=cp936
    • 若locale是utf8的,可以选择
      iocharset=utf8

      但这样做的缺点是会导致vfat模块将允许短文件名使用小写字母,这与windows是不兼容的(使用iocharset=utf8时,内核会出一条警告信息的——这比较没道理,估计是个历史遗留问题);

  • utf8:一个解决办法是使用utf8参数,只要iocharset!=utf8,vfat就会处理大小写的问题;而utf8参数则会最终处理字 符集的转换。utf8参数只能显式地通过mount调用(不能在编译内核时预先指定,当然也可以写在/etc/fstab里),我的意思是这个选项不能偷 懒不写。

最后是结论:

  1. codepage=936,这一选项可以在内核默认设置;
  2. 本地locale是gb系的,参数应为
    mount -t vfat -o iocharset=cp936
  3. 本地locale是utf8的,如果能忍受内核每次都要警告,并且不跟windows交互,可以
    mount -t vfat -o iocharset=utf8
  4. 要是不能忍3或者交互时出现奇怪的问题,那就
    mount -t vfat -o iocharset=cp936,utf8

    实际上只要内核默认的iocharset不是utf8,直接写

    mount -o utf8

    就可以,这里iocharset=xxx的作用仅仅是处理大小写,所以怎么填都没关系。

我们来看一下这些选项究竟做了些什么事情,假设一个FAT文件系统在简体中文的windows上用过,而Linux程序的locale设置为UTF-8,文件名为s,其短文件名为s

  1. 在文件系统上(也就是在硬盘里),文件名被存为s,短文件名被存为φcp936(s)
  2. 内核通过codepage=936选项,将映射φ1cp936作用于φcp936(s),得到
    φ1cp936φcp936(s)=s;
  3. 内核通过utf8或iocharset=utf8选项,将映射φutf8作用于s,s,最终向应用程序输出
    φutf8(s),φutf8(s);
  4. 应用程序(如ls、nautilus)通过locale的设置,将φ1utf8作用于内核输出,得到我们所看见的字符串s,s.
posted @ 2016-04-20 12:23  伴我前行  阅读(1170)  评论(0编辑  收藏  举报