从头到尾快速学习一遍Linux,高级工程师多年实践实战经验精华总结和实例示例,第三章:概念释义

从头到尾快速学习一遍Linux,高级工程师多年实践实战经验精华总结和实例示例,第三章:概念释义。

在这里插入图片描述

第三章 概念释义

理解 Linux 中的基本概念,讲解详细深入。


压缩与解压
####################################

“小王,把我们郊游的照片压缩一下,发给我”,压缩文件在使用电脑时经常见到。将文件压缩不只是为了节省硬盘空间,同时也可以节省网络传输时间、归档文件方便管理等。

压缩可分为无损压缩与有损压缩两种,但不管是采用何种技术模型,其本质内容都是一样的,即通过某种特殊的编码方式将数据信息中存在的重复度、冗余度有效地降低,从而达到数据压缩的目的。例如二进制代码压缩,有 000000,可以把它变成 6 个 0 的写法 60,来减少该文件的空间。解压就是将压缩包中的文件恢复成没有压缩前的样子。

有损压缩

在压缩时去除“不必要”的信息,对文件进行剪裁以使它变得更小,这种压缩方法称为有损压缩。在压缩后将无法复原成原始文件的样子。因此,如果需要完全重现原来的内容(例如软件应用程序、数据库和源代码),应该使用无损压缩。

有损压缩广泛应用于动画、声音和图像文件中,典型的代表就是影碟文件格式 mpeg、音乐文件格式 mp3 和图像文件格式 jpg。

在 Linux 中很多压缩程序只能针对一个文件进行压缩,当要压缩一大堆文件时,首先得先将一堆文件打成一个包(tar 命令),然后再用压缩程序进行压缩(gzip、bzip2 命令),就是说压缩会分为两步,先打包再压缩,各干个的事。

打包


tar 命令可以将多个文件和目录创建一个档案(归档),tar 最初是用来在磁带上创建档案;tar 命令也可以修改档案中的文件,或者加入新的文件;使用 tar 程序打出来的包我们常称为 tar 包,tar 包通常以 .tar 结尾。

归档的一般用途:

* 把一大堆的文件和目录打包成一个 tar 包,便于网络传输。
* 生成tar包后,再用其它的程序进行压缩。

推荐阅读: :ref:tar 归档命令 <cmd_tar>

压缩


gzip

gzip 是 GNUzip 的缩写,它是一个 GNU 自由软件的文件压缩程序,文件经它压缩过后以 .gz 为扩展名,据统计,gzip 命令对文本文件有 60%~70% 的压缩率。gzip 不能用来压缩目录,需要先归档目录,然后在压缩。gzip 和 tar 一起构成了 Linux 操作系统中流行的文件压缩格式(.tar.gz)。

gunzip 命令用来解压缩 gz 文件,事实上 gunzip 就是 gzip 的硬连接,因此不论是压缩或解压缩,都可以通过 gzip 命令单独完成。

推荐阅读: :doc:../Chapter01/00_gzip

bzip2

bzip2 是一款自由软件,以开源软件协议发布的数据压缩算法及程序,以 .bz2 为扩展名结尾。bzip2 只是一个数据压缩工具,而不是归档工具,在这一点上它与 gzip 类似。

bzip2 是一个基于 Burrows-Wheeler 变换的无损压缩软件,压缩效果比传统的 gzip 或 ZIP 的压缩效率更高,但是它的压缩速度较慢。bzip2 利用先进的压缩技术,能够把文件压缩到 10% 至 15%,压缩的速度和解压的效率都非常高,并且支持大多数压缩格式,包括 tar、gzip 等。

推荐阅读: :doc:../Chapter01/00_bzip2

zip

zip 是一个应用广泛的跨平台的打包+压缩工具,压缩文件的扩展名为 .zip

zip 是一种相当简单的分别压缩每个文件的存档格式。分别压缩文件允许不读取额外的数据而检索独立的文件;理论上,允许对不同的文件使用不同的算法。

  • 推荐阅读: :doc:../Chapter01/00_zip
  • 推荐阅读: :doc:../Chapter01/00_unzip

压缩工具对比

- bzip2 gizp 命令会在压缩文件时替换原始文件,tar zip 不会替换
- 除 bzip2 以外,压缩文件的权限将基于 umask 设置。bzip2 会保留原始文件的权限
- zip 创建的压缩文件可以在 Windows 及 MacOS 和其他 Unix 系统即解压,兼容性更强

常用解压/压缩命令


tar

:压缩: tar -cvf FileName.tar DirName
:解压: tar -xvf FileName.tar

.gz

:压缩: gzip FileName
:解压1: gunzip FileName.gz
:解压2: gzip -d FileName.gz

.tar.gz 或 .tgz

:压缩: tar -zcvf FileName.tar.gz DirName
:解压: tar -zxvf FileName.tar.gz

.bz2

:压缩: bzip2 -z FileName
:解压1: bzip2 -d FileName.bz2
:解压2: bunzip2 FileName.bz2

.tar.bz2

:压缩: tar -jcvf FileName.tar.bz2 DirName
:解压: tar -jxvf FileName.tar.bz2

.Z

:压缩: compress FileName
:解压: uncompress FileName.Z

.tar.Z

:压缩: tar -Zcvf FileName.tar.Z DirName
:解压: tar -Zxvf FileName.tar.Z

.zip

:压缩: zip FileName.zip DirName
:解压: unzip FileName.zip

.rar

:压缩: rar -a FileName.rar DirName
:解压: rar -x FileName.rar


一切皆文件
####################################

“一切皆是文件”是 Unix/Linux 的基本哲学之一,它是指 Linux 系统中的所有的一切都可以通过文件的方式访问、管理,即使不是文件,也以文件的形式来管理。例如硬件设备、进程、套接字等都抽象成文件,使用统一的用户接口,虽然文件类型各不相同,但是对其提供的却是同一套操作。

这里的一切是单向的,也即所有的东西都单向通过文件系统呈现,反向不一定可行。比如通过新建文件的方式来创建磁盘设备是行不通的。

不准确但是形象的例子

Linux 系统把硬件设备映射成文件,例如将摄像头映射为 /dev/video,然后就可以使用基本的函数操作它。用 open() 函数连接设备,再用 read() 函数读取图像,最后用 write() 函数保存图像。

而在声卡设备中,read() 函数会变为录音功能,write() 函数变为播放功能。

在 Linux 中共有 7 种类型的文件,分为 3 大类:

  1. 普通文件,包括文本文件和二进制文件
  2. 目录文件(文件夹文件)
  3. 特殊文件
    • :doc:链接文件 <00_link>
    • 字符设备文件
    • 套接字(Socket)文件,用于网络通讯,一般由应用程序创建
    • 命名管道文件
    • 块文件

查看文件类型


ls 命令

使用 :ref:ls -l <cmd_ls> 命令查看文件时,第一位的符号说明:

====== ======
符号 意义
====== ======
普通文件
d 目录文件
l 链接文件
c 字符设备文件
s 套接字(Socket)文件
p 命名管道文件
b 块文件
====== ======

[Linux]$ ls -l
total 8
drwxrwxr-x 2 glenn glenn 4096 Aug 30 21:41 images
- rw-rw-r-- 1 glenn glenn  141 Jul 10 09:53 index.html

file 命令

可以用 :ref:file <cmd_file> 命令,查看某个文件的类型信息:

# 查看普通文件
[Linux]$ file powertop.html
powertop.html: HTML document, ASCII text, with very long lines

# 查看目录文件
[Linux]$ ffile Pictures/
Pictures/: directory

# 查看链接文件
[Linux]$ file log
log: symbolic link to /run/systemd/journal/dev-log

# 查看字符设备文件
[Linux]$ file vcsu
vcsu: character special (7/64)

# 查看查看块文件
[Linux]$ file sda1
sda1: block special (8/1)

# 查看套接字文件
[Linux]$ file system_bus_socket
system_bus_socket: socket

# 查看命名管道文件
[Linux]$ file pipe-test
pipe-test: fifo (named pipe)

stat 命令

也可以用 :ref:stat <cmd_stat> 命令,查看某个文件的类型信息:

# 查看普通文件
[Linux]$ stat 2daygeek_access.log
  File: 2daygeek_access.log
  Size: 14406929    Blocks: 28144      IO Block: 4096   regular file
Device: 10301h/66305d   Inode: 1727555     Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/ daygeek)   Gid: ( 1000/ daygeek)
Access: 2019-01-03 14:05:26.430328867 +0530
Modify: 2019-01-03 14:05:26.460328868 +0530
Change: 2019-01-03 14:05:26.460328868 +0530
 Birth: -
 
# 查看目录文件
[Linux]$ stat Pictures/
  File: Pictures/
  Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 10301h/66305d   Inode: 1703982     Links: 3
Access: (0755/drwxr-xr-x)  Uid: ( 1000/ daygeek)   Gid: ( 1000/ daygeek)
Access: 2018-11-24 03:22:11.090000828 +0530
Modify: 2019-01-05 18:27:01.546958817 +0530
Change: 2019-01-05 18:27:01.546958817 +0530
 Birth: -

# 查看链接文件
[Linux]$ stat /dev/log
  File: /dev/log -> /run/systemd/journal/dev-log
  Size: 28          Blocks: 0          IO Block: 4096   symbolic link
Device: 6h/6d   Inode: 278         Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2019-01-05 16:36:31.033333447 +0530
Modify: 2019-01-05 16:36:30.766666768 +0530
Change: 2019-01-05 16:36:30.766666768 +0530
 Birth: -
    
# 查看字符设备文件
[Linux]$ stat /dev/vcsu
  File: /dev/vcsu
  Size: 0           Blocks: 0          IO Block: 4096   character special file
Device: 6h/6d   Inode: 16          Links: 1     Device type: 7,40
Access: (0660/crw-rw----)  Uid: (    0/    root)   Gid: (    5/     tty)
Access: 2019-01-05 16:36:31.056666781 +0530
Modify: 2019-01-05 16:36:31.056666781 +0530
Change: 2019-01-05 16:36:31.056666781 +0530
 Birth: -

# 查看查看块文件
[Linux]$ stat /dev/sda1
  File: /dev/sda1
  Size: 0           Blocks: 0          IO Block: 4096   block special file
Device: 6h/6d   Inode: 250         Links: 1     Device type: 8,1
Access: (0660/brw-rw----)  Uid: (    0/    root)   Gid: (  994/    disk)
Access: 2019-01-05 16:36:31.596666806 +0530
Modify: 2019-01-05 16:36:31.596666806 +0530
Change: 2019-01-05 16:36:31.596666806 +0530
 Birth: -

# 查看套接字文件
[Linux]$ stat /var/run/dbus/system_bus_socket
  File: /var/run/dbus/system_bus_socket
  Size: 0           Blocks: 0          IO Block: 4096   socket
Device: 15h/21d Inode: 576         Links: 1
Access: (0666/srw-rw-rw-)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2019-01-05 16:36:31.823333482 +0530
Modify: 2019-01-05 16:36:31.810000149 +0530
Change: 2019-01-05 16:36:31.810000149 +0530
 Birth: -

# 查看命名管道文件
[Linux]$ stat pipe-test 
  File: pipe-test
  Size: 0           Blocks: 0          IO Block: 4096   fifo
Device: 10301h/66305d   Inode: 1705583     Links: 1
Access: (0644/prw-r--r--)  Uid: ( 1000/ daygeek)   Gid: ( 1000/ daygeek)
Access: 2019-01-06 02:00:03.040394731 +0530
Modify: 2019-01-06 02:00:03.040394731 +0530
Change: 2019-01-06 02:00:03.040394731 +0530
 Birth: -

inode 索引节点
####################################

inode 是理解 Unix/Linux 文件系统和硬盘储存的基础。

理解 inode,不仅有助于提高系统操作水平,还有助于体会 Unix 设计哲学,即如何把底层的复杂性抽象成一个简单概念,从而大大简化用户接口。

inode 是什么?


理解 inode,要从文件储存说起。文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector),每个扇区储存 512 字节(相当于 0.5KB)。

操作系统读取硬盘的时候,不会一个扇区一个扇区地读取,这样效率太低。而是一次性连续读取多个扇区,即一次性读取一个"块"(block),这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小通常是 4KB,即连续八个 sector 组成一个 block。

文件数据都储存在"块"中,那么还需要一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等。这种储存文件元信息的区域就叫做 inode,中文译名为"索引节点"。

每一个文件都有对应的 inode,里面包含了与该文件有关的信息。

inode 的内容


inode 包含文件的元信息,具体来说有以下内容:

  • 文件的字节数
  • 文件拥有者的 User ID
  • 文件用户组的 Group ID
  • 文件的读、写、执行权限
  • 文件的时间戳,共有三个:
    • ctime 指 inode 上一次变动的时间
    • mtime 指文件内容上一次变动的时间
    • atime 指文件上一次打开的时间
  • 链接数,即有多少文件名指向这个 inode
  • 文件数据 block 的位置

可以用 :ref:stat <cmd_stat> 命令,查看某个文件的 inode 信息:

[Linux]$ stat abc.txt
  File: 'abc.txt'
  Size: 7805      Blocks: 16         IO Block: 4096   regular file
Device: fc09h/64521dInode: 152067      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 5000/shiyanlou)   Gid: ( 5000/shiyanlou)
Access: 2018-01-16 15:14:20.419663570 +0800
Modify: 2018-01-16 15:14:28.331874373 +0800
Change: 2018-01-16 15:14:28.331874373 +0800
 Birth: -

总之,除了文件名以外的所有文件信息,都存在 inode 之中。至于为什么没有文件名,下文会有详细解释。

inode 的大小


inode 也会消耗硬盘空间,所以在格式化硬盘的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是 inode 区(inode table),存放 inode 所包含的信息。

inode 节点一般占用 128 字节或 256 字节。inode 节点的总数,在格式化时会自动设定。一般是每 1KB 或每 2KB 就设置一个 inode。假定在一块 1GB 的硬盘中,每个 inode 节点的大小为 128 字节,每 1KB 就设置一个 inode,那么 inode table 的大小就会达到 128MB,占整块硬盘的 12.8%。

查看每个硬盘分区的 inode 总数和已经使用的数量,可以使用 :ref:df <cmd_df> 命令。

[Linux]$ df -i
Filesystem
   Inodes  IUsed    IFree IUse%  Mounted on
/dev/mapper/docker
  786432  109887   676545   14% /
tmpfs
  1021949     17  1021932    1% /dev
tmpfs
  1021949     11  1021938    1% /sys/fs/cgroup
/dev/xvdc1
 19660800  10120 19650680    1% /etc/hosts
shm
  1021949      1  1021948    1% /dev/shm

查看每个 inode 节点的大小,可以用如下命令(需要 root 权限):

[Linux]# dumpe2fs -h /dev/hda | grep "Inode size"
dumpe2fs 1.41.14 (22-Dec-2010)
Inode size:                256

由于每个文件都必须有一个 inode,因此有可能发生 inode 已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

inode 号码


每个 inode 都有一个号码,操作系统用 inode 号码来识别不同的文件。

这里值得重复一遍,Unix/Linux 系统内部不使用文件名,而使用 inode 号码来识别文件。对于系统来说,文件名只是 inode 号码便于识别的别称或者绰号。

表面上,用户通过文件名打开文件。实际上,系统内部将这个过程分成三步:

  1. 系统找到文件名对应的 inode 号码
  2. 通过 inode 号码,获取 inode 信息
  3. 根据 inode 信息,找到文件数据所在的 block,读出数据。

使用 :ref:ls -l <cmd_ls> 命令查看文件名对应的 inode 号码:

[Linux]$ ls -i abc.txt
152067 abc.txt

目录文件


Unix/Linux 系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的 inode 号码。所以在使用 :ref:ls -lh <cmd_ls> 命令查看文件夹大小时,所有的文件夹只有 4KB 大小。

ls 命令只列出目录文件中的所有文件名:

[Linux]$ ls -lh
total 48K
drwxr-xr-x  2 glenn glenn 4.0K Jul  9 14:59 Desktop
drwxr-xr-x 10 glenn glenn 4.0K Dec  3 17:39 Documents
drwxr-xr-x  2 glenn glenn  12K Nov 30 13:46 Downloads
drwxr-xr-x  3 glenn glenn 4.0K Nov 30 14:53 Music
drwxr-xr-x  2 glenn glenn 4.0K Nov 30 14:54 Pictures
drwxr-xr-x  2 glenn glenn 4.0K Nov 22 20:33 Public
drwxr-xr-x  2 glenn glenn 4.0K Jul  9 14:59 Templates
drwxr-xr-x  8 glenn glenn 4.0K Oct 19 22:52 Videos

如果要查看文件的详细信息,就必须根据 inode 号码,访问 inode 节点,读取信息。

目录的执行权限

目录文件的读权限(r)和写权限(w)只是针对目录文件本身。由于目录文件内只有文件名和 inode 号码,所以如果只有读权限,只能获取文件名,无法获取其他信息。



    [Linux]$ ls -l 
    drw-rw-rw-  2 glenn glenn  4096 Dec  5 18:11 test/
    [Linux]$ls -l test/
    total 0
    d????????? ? ? ? ?            ? ./
    d????????? ? ? ? ?            ? ../
    -????????? ? ? ? ?            ? b.txt

要读取 inode 节点内的信息具有文件夹的执行权限(x)。

inode 的特殊作用


由于 inode 号码与文件名分离,这种机制导致了一些 Unix/Linux 系统特有的现象。

  1. 当文件名包含特殊字符,无法正常删除时。可以删除 inode 节点,就能直接删除文件。
  2. 移动文件或重命名文件,只是改变文件名,不影响 inode 号码。所以在 Linux 中移动文件不论大小基本秒成。
  3. 打开一个文件后,系统就以 inode 号码来识别文件,不再考虑文件名。因此,系统无法从 inode 号码得知文件名。

第 3 点使得在更新软件时可以不关闭、不重启软件。因为系统通过 inode 号码,识别运行中的文件,软件更新的时候,新版文件以同样的文件名,生成一个新的 inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的 inode 则被回收。


硬链接和软链接
####################################

在 Linux 中有两种 link(链接)的概念,一般称之为硬链接和软链接(或符号链接)。

硬链接


一般情况下,文件名和 inode 号码是"一一对应"的关系,每个 inode 号码对应一个文件名(每个文件默认有一个硬链接)。但是,Unix/Linux 系统允许多个文件名指向同一个 inode 号码。

这意味着,可以用不同的文件名访问同样的内容,对文件内容进行修改后,会影响所有文件名。但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。

创建一个硬链接,就会为文件创建了一个新的文件名。硬链接有两个重要局限性:

  1. 硬链接不能链接不在同一系统的文件。也就是说硬链接不能链接与文件不在同一磁盘分区上的文件;
  2. 硬链接不能链接目录。

一个硬链接和文件本身没有什么区别。当你列出一个包含硬链接的文件时,不会有特殊的链接指示说明。当一个硬链接被删除时,文件本身的内容仍然存在(也就是说,它所占的磁盘空间不会被重新分配),直到所有关联这个文件的硬链接都删掉。

使用 :ref:ln <cmd_ln> 命令创建硬链接:

[Linux]$ ln abc.txt def.txt
[Linux]$ ls -li
total 24
152410 drwxrwxr-x 2 shiyanlou shiyanlou 4096 Aug 17  2016 Desktop
152067 -rw-rw-r-- 2 shiyanlou shiyanlou 7805 Jan 16 15:14 abc.txt
152067 -rw-rw-r-- 2 shiyanlou shiyanlou 7805 Jan 16 15:14 def.txt

运行上面的命令后,多出一个 def.txt 文件,而且两个文件的 inode 号相同。inode 信息中的“链接数”会增加 1。

反过来,删除一个文件名,就会使得 inode 节点中的"链接数"减 1。当这个值减到 0,表明没有文件名指向这个 inode,系统就会回收这个 inode 号码,以及其所对应 block 区域。

目录文件的硬链接

创建目录时,默认会生成两个目录项: ``.`` 和 ``..`` 。 ``.`` 相当于当前目录的硬链接; ``..`` 相当于父目录的硬链接。所以,目录的硬链接总数,等于 2 加上它的子目录总数(含隐藏目录)。其实使用 ``ln -d`` 命令也允许 root 用户尝试建立目录硬链接。

这些都说明系统限制对目录进行硬链接只是一个硬性规定,并不是逻辑上不允许或技术上不可行。为什么操作系统要进行这个限制呢?

由于 Linux 操作系统中的目录是以 ``/`` 为节点的树状结构,对目录的硬链接有可能破坏这种结构,甚至形成循环如: ``/usr/bin -> /usr/`` ,在使用遍历目录的命令时(如: ``ls -R`` )系统就会陷入无限循环中。软链接的 inode 号码不一样,所以不会出现这种问题。

软链接(或符号连接)


除了硬链接以外,还有一种软链接,创建软链接是为了克服硬链接的局限性。

软链接是通过创建一个特殊类型的文件(指针)链接到文件或目录。就像是 Windows 的快捷方式,当然,符号链接早于 Windows 的快捷方式很多年。

文件 A 和文件 B 的 inode 号码虽然不一样,但是文件 A 的内容是文件 B 的路径。读取文件 A 时,系统会自动将访问指向文件 B。因此,无论打开哪一个文件,最终读取的都是文件 B。但是,文件 A 依赖于文件 B 而存在,如果删除了文件 B,打开文件 A 就会报错:“No such file or directory”。这是软链接与硬链接最大的不同:文件 A 指向文件 B的文件名,而不是文件 B 的 inode 号码。

软链接的应用

想象这样一个情景,一个程序需要使用 foo_1.1 文件中的共享资源,由于 foo 经常改变版本号。每次升级后都得将使用 foo_1.1 的所有程序更新到 foo_1.2 文件,那么每次更新 foo 版本后,都要重复上边的工作。

符号链接能很好的解决这个问题。比如,创建一个 foo 的软链接指向 foo_1.2。这时,当一个程序访问 foo 时,实际上是访问 foo_1.2。当升级到 foo_1.3 时,只需要更新软链接指向。这不仅解决了版本升级问题,而且还允许在系统中保存两个不同的版本,如果 foo_1.3 有错误,再更新回原来的 foo_1.2 链接就可以。

使用 :ref:ln -s <cmd_ln> 命令创建软链接:

[Linux]$ ln -s abc.txt aaa
[Linux]$ ls -l
total 8
lrwxrwxrwx 1 shiyanlou shiyanlou    7 Jan 16 17:49 aaa -> abc.txt
-rw-rw-r-- 1 shiyanlou shiyanlou 7805 Jan 16 17:49 abc.txt

浅析 Linux 的国际化与本地化机制
################################

什么是国际化和本地化


不同的国家和地区因文化的差异,在日期、时间以及货币符号等表示方式上都不完全相同,最为明显的就是语言。有时在编写软件给用户使用时,开发者、维护者以及最终用户可能分别来自不同的区域,而要求他们均使用同一种语言显然是不明智的,因此当一个程序或者软件编写给全世界人使用时,通常分为两个部分:国际化(internationalization 即缩写为 i18n,这是由于在这个单词的头尾之间包含了 18 个字母)和本地化 (localization,缩写为 l10n)。

国际化,指的是一个程序或软件可给特定的人群使用而无须修改或重新编译源代码。在 ISO C 中,国际化的工作依赖于 locales。程序开发者可使用多样的方式来国际化他们的程序,但是 GNU gettext 已成为了其中的一种标准。

本地化,指的是一个程序或软件在支持国际化的基础上,给定程序特定区域的语言信息使其在信息的输入输出等处理上适应特定区域人群的使用。这里允许程序所使用的一些语言环境变量在程序执行时动态配置。

简单的说,国际化是开发者的任务,是一个一般化的过程;而本地化则是翻译者所做的事情,是一个具体的过程。国际化的运作为本地化工作提供了可能。对于国际化与本地化,有时我们也称为 NLS(Native Language Support)。Glibc(GNU C library)作为 Linux 的 C 标准库为 Linux 处理国际化与本地化提供了基础,Linux 上的程序处理依赖于 glibc(见下图)。

.. image ../Images/locale.01.jpg

使用和设定系统 locale


对用户而言,用来控制语言或区域环境生效的功能就叫做 locale。locale 是 glibc 的一个重要组成部分,也是 Linux 国际化和本地化工作的一个重要基础。locale 通过设置一系列的环境变量来满足用户对国际化和本地化的需求。通过 locale 命令,我们不仅可查看到语言环境的当前设置,还可查看当前 locale 可用的名称和字符集。

整个系统使用的 locale 可以通过创建或编辑 /etc/locale.conf 来配置,或者通过 localectl 设置。

用户可以自定义 locale 配置文件,以实现个性化。用户可以创建自己的 $HOME/.config/locale.conf 来覆盖系统配置文件。

locale 变量的值有三个要素:语言代码 (Language Code)、 地域代码 (Country Code) 和字符集(Encoding)

[gavin@rudder ~]$ locale
# 未设置任何 LC_xxx 变量时所使用的默认值 LANG
LANG=en_US.UTF-8
# 使用 gettext 翻译的软件会按照先后顺序选择语言
LANGUAGE=zh_CN:en_GB:en
# 指定使用某区域的字符分类及处理方式
LC_CTYPE=zh_CN.UTF-8
# 指定使用某区域的非货币的数字格式
LC_NUMERIC="en_US.UTF-8"
# 指定使用某区域的日期和时间格式
LC_TIME="en_US.UTF-8"
# 指定使用某区域的排序规则
LC_COLLATE="en_US.UTF-8"
# 指定使用某区域的货币格式
LC_MONETARY="en_US.UTF-8"
# 指定使用某区域的响应与信息的格式
LC_MESSAGES="en_US.UTF-8"
# 指定使用某区域的纸张大小
LC_PAPER="en_US.UTF-8"
# 指定使用某区域的姓名书写方式
LC_NAME="en_US.UTF-8"
# 指定使用某区域的地址格式和位置信息
LC_ADDRESS="en_US.UTF-8"
# 指定使用某区域的电话号码格式
LC_TELEPHONE="en_US.UTF-8"
# 指定使用某区域的度量衡规则
LC_MEASUREMENT="en_US.UTF-8"
# 对 locale 自身信息的概述
LC_IDENTIFICATION="en_US.UTF-8"
# 用来覆盖掉所有其他 LC_xxx 变量的值
LC_ALL=

除上边所说的,另有用于指定 locale 可用名称目录的变量 LOCPATH,默认使用的路径是 /usr/lib/locale/ 。另外,当一个程序找寻 locale 环境变量值时,它会依以下优先顺序使用变量。

[1] LANGUAGE
[2] LC_ALL
[3] LC_xxx
[4] LANG

其中 LC_ALL 并不是一个环境变量,它仅是一个可被函数 setlocale (setlocale 的函数原型及其参数 category 的可用值均被定义在头文件 locale.h 中)调用的宏,它的值可覆盖所有其他的 locale 设定 (如果 LC_ALL 的值存在的话,即非空) 。LANG 用于正常指定使用某区域环境值,而 LANGUAGE 则用于指定个人对语言环境值的主次偏好。通常我们会在设定 LANG 后,并通过 LC_xxx 加以修正。

LANGUAGE="en_US:en"
LANG="en_US.UTF-8"
LC_CTYPE="zh_CN.UTF-8"

也可以将以上内容添加至 /etc/profile/etc/environment 等系统初始文件中,以保证系统在启动后立即使用您所期望的语言环境。值得注意的是,若 locale 被设定为 “C”,那么 LANGUAGE 的值将被忽视。因此我们必须为 LANG (或 LC_ALL) 设置有效的 locale 名称,而非 “C”。

# 当前系统可用的 locale 名称
[gavin@rudder ~]$ locale – a
C
en_AU.utf8
...
POSIX
zh_CN.utf8
...

上边我们看到了两个特别的 locale 名称,C 和 POSIX。当前,POSIX 仅是 C 的一个别名。除 C 和 POSIX 之外,locale 的名称并未标准化。同时,上边可用名称的输出已依 LC_COLLATE 所指定的排序规则进行了排序。另外,我们看到 locale 的名称存在一个命名的格式。

language[_territory[.codeset]][@modifier]

其中 language 是 ISO 639-1 标准中定义的语言代码,territory 是 ISO 3166-1 标准中定义的国家和地区代码,codeset 是字符集的名称( 如 UTF-8 等 ),而 modifier 则是某些 locale 变体的修正符。若期望使用的 locale 名称未在以上的列表中,那么我们可使用 glibc 提供的命令 localedef 进行添加(命令 localedef 会在相关路径生成必要的数据文件)。

# 通过命令 localedef 添加 fi_FI.UTF-8
[1] localedef -f UTF-8 -i fi_FI fi_FI.UTF-8
[2] localedef -f UTF-8 -i fi_FI ./fi_FI

方式 1 将在默认路径上生成一个 locale-archive 文件,而方式 2 则在指定路径产生一个目录,该目录中将包含 locale 相关的数据。另外,命令 localedef 还提供了 --no-archive 选项,该选项可使方式 1 生成的也是一个目录,而非 locale-archive 文件。下面我们通过设置 LC_ALL 和 LC_TIME 的值来了解该 locale 环境变量对时间和日期格式的影响,从而更好的理解 locale 环境变量在系统上的基础作用(如下,另外我们需在运行前确定该 locale 名称是有效的)。

# locale 环境变量对系统命令的影响
[gavin@rudder ~]$ LC_ALL=en_US.UTF-8 date
Thu Nov  5 14:13:36 CST 2009

[gavin@rudder ~]$ LC_TIME=fi_FI.UTF-8 date
to 5.11.2009 14.13.44 +0800

[gavin@rudder ~]$ LC_ALL=zh_CN.UTF-8  locale  -ck  LC_TIME
LC_TIME
abday="日 ; 一 ; 二 ; 三 ; 四 ; 五 ; 六"
...
...
date_fmt="%Y 年 %m 月 %d 日 %A %H:%M:%S %Z"
time-codeset="UTF-8"

在这里另要指出的是,由 GNU coreutils 提供的 date 命令在实现时加入了以下内容,而这正是 date 命令实现国际化与本地化的关键。

# date 命令的源码片断
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);

字符及字符集处理


字符以及字符集的处理,是 Linux 国际化与本地化的另一重要内容。在计算机的早期字符集中,每个字符仅使用 6 个、7 个或 8 个位(bits),但这对于类似东方语系的来说是明显不够的,因此出现了双字节字符集甚至更多字节。在字符集编码上有着两个重要的概念,即内码与外码。内码是在计算机内存中使用的编码,而外码则是在计算机外部使用的编码,如存储和传输等。常用的宽字符集内码有 Unicode 和 ISO 10646(也叫做 UCS,即 Universal Character Set)。Unicode 的最初设计是 16 位,而 ISO 10646 使用的是 31 位。经发展这两个标准现几乎没有差异,Unicode 标准对应于 ISO 10646 实现级别 3(即支持所有的 UCS 组合字符)。而我们常用的 UTF-8 即 UCS 变形格式 8,是一种兼容于 ASCII 编码和所有 POSIX 文件系统的可变长编码。

通常,程序要依靠一些分类函数来处理字母、数字及空白等字符,而这些函数是会受到当前 locale 中的 LC_CTYPE 值的影响的。在 ISO C 标准中描述了两种不同的字符处理方式,即是 char 类型和 wchar_t 的宽字符(即 wc)。它们的分类函数分别被定义在头文件 ctype.h 和 wctype.h 中。对于多字节字符串(mbs)以及宽字符串(wcs)的处理函数则被定义在头文件 wchar.h 中。显然,宽字符分类函数更为通用,因为它允许字符集分类的扩展可超过它的可用值。而这在 POSIX 中有描述的字符集扩展已在 glibc 的程序 localedef 中实现。另外,glibc 考虑到 locale 对程序字符的影响,提供了独立于 locale 的更为通用的字符集处理函数及工具 iconv。

# iconv 相关函数
#include <iconv.h>
iconv_t iconv_open(const char *tocode,
                  const char *fromcode);
int iconv_close(iconv_t cd);
size_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft,
                  char **outbuf, size_t *outbytesleft);

下面我们通过查看文件系统 /proc 中的内容,以此来观察 Linux 国际化与本地化机制在内存中的情况(如下)。使用命令 cat 查看到的 /proc/self/maps 文件内容应与命令 locale 返回的系统当前区域环境值相一致(即将下边与第一个相比较)。

# 当前进程的内存映射及访问权限
[gavin@rudder ~]$ cat /proc/self/maps
...
085a2000-085c3000 rw-p 085a2000 00:00 0          [heap]
b7a90000-b7acf000 r--p 00000000 08:08 740190     /usr/lib/locale/zh_CN.utf8/LC_CTYPE
b7acf000-b7ad0000 r--p 00000000 08:08 729171     /usr/lib/locale/en_US.utf8/LC_NUMERIC
b7ad0000-b7ad1000 r--p 00000000 08:08 781364     /usr/lib/locale/en_US.utf8/LC_TIME
b7bbd000-b7dbd000 r--p 00000000 08:08 704987     /usr/lib/locale/locale-archive
b7dbd000-b7dbe000 rw-p b7dbd000 00:00 0
b7dbe000-b7f1a000 r-xp 00000000 08:08 852866     /lib/tls/i686/cmov/libc-2.9.so
...
b7f27000-b7f2e000 r--s 00000000 08:08 704989     /usr/lib/gconv/gconv-modules.cache
...
b7f32000-b7f4e000 r-xp 00000000 08:08 827406     /lib/ld-2.9.so
...

在上边中我们看到了 locale-archive 文件,如上述这个文件等同于其他含 locale 相关数据的目录文件(如 /usr/lib/locale/en_US.utf8/LC_TIME 等)。文件 gconv-modules.cache 是由命令 iconvconfig 以文件 gconv-modules 生成的 iconv 配置缓存文件,但该文件并不影响 iconv 的使用。当 gconv-modules.cache 不存在时,iconv 将会尝试打开配置文件 gconv-modules。

简述 gettext


GNU gettext 是为程序实现国际化与本地化而设计的,它作为 GNU 实现软件翻译项目的一个重要部分为程序开发者、翻译者甚至用户提供了一个可生成多语言信息的框架(通过 gettext 提供的一些接口可使程序与系统的语言或区域环境相一致)。由此 gettext 可帮助我们快速的完成程序或软件的国际化与本地化,而 gettext 工具集则帮助我们更好的管理和维护翻译文档,其具体包含了:

  • 一组关于如何编写程序来支持信息分类的约定;

  • 一个支持信息分类的目录以及文件的命名管理;

  • 一个支持获取翻译信息的动态库文件;

  • 一些用于管理翻译信息(或已翻译文件)的独立程序;

  • 一个支持解析、创建翻译信息的库文件;

  • 一个专为 Emacs 提供的模块,方便设置、获取时间戳。

在这里插入图片描述

上图为使用 gettext 工具集处理程序国际化和本地化的流程。事实上,在 glibc 中提供了两组不同的接口来完成信息的翻译,但是它们都没被 POSIX 标准所接受。其中一组接口就是 gettext,而另一组接口则是 catgets。虽然 catgets 被定义进了 X/Open 标准中,但这是来着产业的决定,因此它可能并不合理。相比于 gettext 方案,catgets 的不足之处是函数 catgets 的第三个参数(即消息 ID)是唯一的,这将导致程序编写者及翻译者在字符信息管理和维护上的困难,因此建议使用 gettext。下边是一些与 catgets 相关的函数。

# 与 catgets 相关的一些函数
#include <nl_types.h>
nl_catd catopen(const char *name, int flag);
int catclose(nl_catd catalog);
char *catgets(nl_catd catalog, int set_number,
                     int message_number, const char *message);

一些相关的函数


在 Linux 实现国际化和本地化时的一个重要函数就是 setlocale(),我们可以在系统工具集(如 date 命令、命令 locale 及 localedef 均有使用此函数)或是一些软件(如文本编辑软件 gedit)的源码中发现其存在的痕迹。

#include <locale.h>
char *setlocale(int CATEGORY, const char *LOCALE);

函数 setlocale 被用于设置或查询程序当前的 locale 值。参数 CATEGORY 一共有 13 个可用值(如 LC_COLLATE 等),它们被定义在头文件 locale.h 中,分别表示从 0 至 12 的整数值。第二个参数 LOCALE 应为 locale 名称(如 zh_CN.UTF-8 等),但是仍有两个特殊的值可使用 “” 和 NULL。如果参数 LOCALE 的值是 “”,则函数返回系统当前的环境值(即置程序的环境值与系统相一致)。若 LOCALE 的值为 NULL,则仅返回程序当前 locale 的设置。程序在设置 locale 值之前,其环境值默认是 “C” 或 “POSIX”。对于国际化程序,函数 setlocale 总被调用,同时为了保持程序的通用性,我们一般把 LOCALE 置为 “”。另外,在 清单 6中的另两个函数均被定义在头文件 libintl.h 中。

#include <libintl.h>
char * textdomain (const char * domainname);

函数 textdomain 的作用是重设当前的 domain 值以供 gettext() 等函数使用,其参数 domainname 为期望设置的新的 domain 值。如果参数 domainname 为 “”,那么函数将返回默认值 “messages”,但是似乎没人愿意使用该值,因为那样会使程序间出现冲突以至混乱。若 domainname 值为 NULL,则返回程序当前的 domain 值(在先前没有设置的情况下,仍会返回预定值 “messages”)。另外需注意的是,dcgettext() 等自带有参数 domainname 的函数,将不受函数 textdomain 的影响(除非有人把它们的参数 domainname 值设为 NULL)。

#include <libintl.h>
char * bindtextdomain (const char * domainname, const char * dirname);
char * bind_textdomain_codeset (const char * domainname,
const char * codeset);

函数 bindtextdomain 的作用是通过给定的 domain 查找路径,可用信息的路径被指定为:

dirname/locale/category/domainname.mo

dirname 即为参数 dirname,若参数 dirname 为 NULL,则函数返回程序当前路径值(默认是 /usr/share/locale),若 dirname 值为 “”,则返回值为空。locale 即为 locale 名称,category 是 locale 的分类,如 LC_MESSAGES 等。domainname 即为参数 domainname。函数 bind_textdomain_codeset 的功能与 bindtextdomain 相近,因此 glibc 在实现时采用了一个内部函数 set_binding_values 并通过对该函数输入参数的控制分别实现以上 2 个函数。

# glibc 中实现函数 bindtextdomain
/* intl/bindtextdom.c */
static void
set_binding_values (domainname, dirnamep, codesetp)
    const char *domainname;
    const char **dirnamep;
    const char **codesetp;
{ ... }

/* 函数 bindtextdomain */
char *
BINDTEXTDOMAIN (domainname, dirname)
    const char *domainname;
    const char *dirname;
{
 set_binding_values (domainname, &dirname, NULL);
 return (char *) dirname;
}

/* 函数 bind_textdomain_codeset */
char *
BIND_TEXTDOMAIN_CODESET (domainname, codeset)
    const char *domainname;
    const char *codeset;
{
 set_binding_values (domainname, NULL, &codeset);
 return (char *) codeset;
}

另外,还有一个可访问 locale 相关信息的重要函数 nl_langinfo,该函数定义于头文件 langinfo.h 中,其作用是通过给定的 item 返回与 locale 相关的信息。

# 函数 nl_langinfo
#include <langinfo.h>
char *nl_langinfo(nl_item item);

/* locale/nl_langinfo.c */
char *
nl_langinfo (item)
    nl_item item;
{
 return __nl_langinfo_l (item, _NL_CURRENT_LOCALE);
}

简单示例


通过以上的描述我们大致了解了 Linux 对国际化与本地化的支持,下面我们编写一个获取当前系统时间的小程序来更好的理解函数 setlocale 对整个程序的影响。

# 使用了函数 setlocale 的当前系统时间获取程序
#include <time.h>
#include <locale.h>
#include <stdio.h>

#define SIZE 80

int main (int argc, char *argv[])
{
   time_t now;
   struct tm *timeinfo;
   struct lconv *lc;
   char buffer[SIZE];

   setlocale (LC_ALL, "");
   printf ("LC_TIME = %s\n", setlocale (LC_TIME, NULL));
   printf ("LC_MONETARY = %s\n", setlocale (LC_MONETARY, NULL));

   time (&now);
   timeinfo = localtime (&now);

   strftime (buffer, SIZE, "%c", timeinfo);
   printf ("Date : %s\n", buffer);

   lc = localeconv();
   printf ("Currency symbol : %s\n", lc->currency_symbol);

   return 0;
}

$ gcc -Wall locale-time.c -o locale-time
$ LC_ALL=zh_CN.UTF-8 ./locale-time
LC_TIME = zh_CN.UTF-8
LC_MONETARY = zh_CN.UTF-8
Date : 2009 年 11 月 05 日 星期四 19 时 47 分 46 秒
Currency symbol : ¥

上边,我们不仅展示了 setlocale 函数的 “” 和 NULL 这两个特殊参数值的使用,还使用了 lconv 这个数据结构用于打印与区域相一致的货币符号(lconv 这个有关数字和货币规则信息的结构体及相关函数 localeconv 被定义在头文件 locale.h 中,与 locale 中的 LC_NUMERIC 和 LC_MONETARY 相关)。在程序执行时,我们动态的修改了 locale 环境变量的值以此来更好的观察 setlocale 函数对程序的影响(如前述使用 date 命令时一样,见清单 5)。在没有调用函数 setlocale 的程序中,程序将使用默认环境值 “C” 或 “POSIX”。由于我们在上面提过 glibc 提供了两组不同的接口实现程序的国际化与本地化,为了更好的理解这两种方式,我们在下面分别进行展示。

# gettext 使用示例
#include <locale.h>
#include <libintl.h>
#include <stdio.h>

#define PACKAGE "gettext-hello"
#define LOCALEDIR "po"
#define N_(msgid) gettext(msgid)

int main (int argc, char *argv[])
{
   setlocale (LC_CTYPE, "zh_CN.UTF-8");
   setlocale (LC_MESSAGES, "zh_CN.UTF-8");

   bindtextdomain (PACKAGE, LOCALEDIR);
   textdomain (PACKAGE);

   /* Translators: 这里仅是一个注释 */
   printf (N_("Are you ok?\n"));

   return 0;
}

我们指定使用 locale 名字为 “zh_CN.UTF-8” 以此方便我们建立目录进行测试,但是更通常的做法是使用 “” 作为参数值使程序适应不同的语言(区域)环境。另外,我们需要为这个简单的程序做一个翻译,并生成一个可用的二进制翻译文件。我们使用由 GNU gettext 提供的工具 xgettext 及 msgfmt 来完成翻译档的生成,但这仅是用于制作、维护 PO(Portable Object)和 MO(Machine Object)文件的部分工具集。

# 执行 gettext 示例程序
$ xgettext --add-comments  --keyword=N_ gettext-hello.c -o \
 > gettext-hello.pot --from-code=UTF-8
 $ cp gettext-hello.pot gettext-hello.po
 $ cat locale-hello.po
 ...
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
 ...
 #. Translators: 这里仅是一个注释
 #: locale-hello.c:23
 msgid "Are you ok?\n"
 msgstr "你还好吗? \n"

 $ mkdir -p po/zh_CN.UTF-8/LC_MESSAGES/
 $ msgfmt gettext-hello.po -o gettext-hello.mo
 $ mv gettext-hello.mo po/zh_CN.UTF-8/LC_MESSAGES/
 $ unset LANGUAGE
 $ gcc -Wall gettext-hello.c -o gettext-hello
 $ LC_ALL=zh_CN.UTF-8 ./ gettext-hello
你还好吗?




# catgets 使用示例
#include <nl_types.h>
#include <locale.h>
#include <stdio.h>

#define CATALOG_NAME "catgets-hello.cat"

int main (int argc, char *argv[])
{
   nl_catd catd;
   setlocale (LC_ALL, "");
   printf ("LC_MESSAGES = %s\n", setlocale (LC_MESSAGES, NULL));

   catd = catopen (CATALOG_NAME, NL_CAT_LOCALE);
   if(catd == (nl_catd) -1) {
       perror("catopen");
       return 1;
   }

   int set_no=11;
   int msg_id=14;
   printf("%s\n", catgets (catd, set_no, msg_id, "Are you OK?"));

   if(catclose(catd) < 0) {
       perror ("catclose");
       return 1;
   }

   return 0;
}

我们在 catgets 示例代码中加上了错误处理,但这仅是为了更好的展示。通常情况下这是不需要的,因为我们应尽量使程序运行下去而不是中断。在编辑完程序所需的翻译档后,我们执行这个简单的 catgets 示例。

# 执行 catgets 示例程序
$ cat catgets-hello.msg
 ...
 $set 11
 14 你还好吗?
 15 I am fine,thanks.
 ...

 $ gencat catgets-hello.msg -o catgets-hello.cat
 $ mv catgets-hello.cat  po/zh_CN.UTF-8/LC_MESSAGES/
 $ gcc -Wall catgets-hello.c -o catgets-hello
 $ export NLSPATH=po/%L/LC_MESSAGES/%N
 $ LC_ALL=zh_CN.UTF-8 ./ catgets-hello
 LC_MESSAGES = zh_CN.UTF-8
你还好吗?

我们使用命令 gencat 生成程序所需的二进制翻译档并通过 NLSPATH 指定该文件存放的位置。若不指定该变量,默认的位置将是如下所示。

[1] prefix/share/locale/%L/%N
[2] prefix/share/locale/%L/LC_MESSAGES/%N

其中 %L 用于指定 locale 名字,%N 为文件名。对于上面几个示例及命令,更好的执行方式是通过 strace 等调试工具进行跟进,这不仅能熟悉相关函数的作用,并可更好的理解 Linux 国际化与本地化机制。

结束语


从国际化与本地化的概念深入到 Linux 实现国际化与本地化机制的 glibc 源码,并尝试从多个角度理解 Linux 上的国际化与本地化机制的运作,最后我们还编写了一些示例,但仍有一些内容被我们忽略,如多语言化、X Window 系统的国际化环境等。另外,一个值得关注的项目是 uClibc,它是一个为嵌入式 Linux 设计的 C 库,相比于 glibc 小巧许多但功能及实现上稍有差异。

扩展阅读:

  • 查阅“glibc 手册”,获取更多 glibc 的知识。 <https://www.gnu.org/software/libc/manual/>_

  • 查阅“gettext 工具集手册”,了解有关 GNU gettext 工具集的内容。 <https://www.gnu.org/software/gettext/manual/gettext.html>_

  • 查看文章“Locale tutorial”,了解关于 locale 和 catgets 的内容。 <http://www.kulichki.com/moshkow/CYRILLIC/locale-tutorial-0_8.txt>_

  • 查阅“UTF-8 and Unicode FAQ for Unix/Linux”文档,了解更多有关字符集的知识。 <http://www.cl.cam.ac.uk/~mgk25/unicode.html>_

  • How to change system locale on RHEL7? <https://access.redhat.com/solutions/974273>_


安全模型与权限
################################

在 Linux 系统中,所有的操作实质上都是在进行进程访问文件的操作。在访问文件之前需要取得相应的权限,而权限是通过 Linux 系统中的安全模型获得的。理论上进程所拥有的权限与执行它的用户的权限相同。其中涉及的一切内容,都是围绕这个核心进行的。

Linux 系统中的安全模型,有两种类型:

  1. Linux 系统上最初的安全模型叫自主访问控制(DAC 全称 Discretionary Access Control)

  2. 后来又增加设计了一个新的安全模型叫强制访问控制(MAC 全称 Mandatory Access Control)

MAC 和 DAC 不是互斥的,DAC 是最基本的安全模型,也是最常用的访问控制机制,是 Linux 必须具有的功能;而 MAC 是构建在 DAC 之上的加强安全机制,属于可选模块。

为区分两者,我们将支持 MAC 的 Linux 系统称作 SELinux,表示它是针对 Linux 的安全加强系统。

用户和组信息


用户和组分别用 UID 和 GID 表示,一个用户可以同时属于多个组,默认每个用户必属于一个与之 UID 同值同名的 GID。

系统保存用户信息的文件是 /etc/passwd ,保存组信息的文件是 /etc/group ,保存密码口令及其变动信息的文件是 /etc/shadow

[Linux]$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
...

在 passwd 文件中每条记录分别为:

  • 用户名

  • 密码口令(在 /etc/shadow 中加密保存)

  • UID

  • GID(默认 UID)

  • 描述注释

  • 主目录

  • 登录 shell(第一个运行的程序)

    [Linux]$ cat /etc/group
    root❌0:
    tty❌5:
    cdrom❌24:xiao,da
    ftp❌113:

在 group 文件中每条记录分别为:

  • 组名

  • 密码口令(一般不存在组口令)

  • GID

  • 组成员用户列表(逗号分割的用户 UID 列表)

    [Linux]# cat /etc/shadow
    root: 6 6 6N3FOQjepEb0oy2FynGXqNTpH0eAWe4UF0:18843:0:99999:7:
    sshd::18843:0:99999:7:
    xiao: 6 6 6u9TxaH3T.Ks.4wLeZgC9aEzBI8X7AYvW9:18843:0:99999:7:
    ftp:
    :18843:0:99999:7:

在 shadow 文件中每条记录分别为:

  • 登录名
  • 加密口令
  • 最后一次修改时间
  • 最小时间间隔
  • 最大时间间隔
  • 警告时间
  • 不活动时间

文件权限控制


在 Linux 中 :doc:一切皆为文件 <00_file> ,对文件的权限分三组进行控制:

  • user 对文件属主设定的权限
  • group 对文件属组设定的权限
  • others 对其他者设定的权限

常用的可设定的权限值,包括:

  • r 读权限
  • w 写权限
  • x 执行权限
  • s 强制位权限
  • t 粘滞位权限
  • i 不可修改权限
  • a 只追加权限

强制位和粘滞位

强制位权限可以使其他用户临时拥有文件所有者的身份。典型的是 ``/etc/shadow`` 文件,当用户修改密码时需要获得 root 权限。

强制位权限针对可执行文件和目录,包含 S_ISUID、S_ISGID 两个常量;

- S_ISUID 只能应用于二进制可执行文件(shell 脚本不是二进制文件)
- S_ISGID 可应用于二进制可执行文件和目录,当 S_ISGID 用于目录时,用户进程的用户组 ID 将会设置为该目录的用户组。

粘滞位权限,一般对目录针对 others 设置,设置后在目录中只有属主和 root 有删除文件的权限,即用户只能删除自己为属主的文件(多用于共享目录中)。



当对一个不具备 x 权限的文件设置 s 权限时无效,权限变为大写 S,表明 s 权限未生效。

目录的 x 权限

当目录只有读取权限时,是无法用 :ref:cd 命令 <cmd_cd> 打开或用 :ref:ls 命令 <cmd_ls> 列出目录中的文件信息的。需要读取目录中的文件时,此目录必须具备 x 权限。

====== ====== ====== ====== ======
权限 cd ls cat touch
====== ====== ====== ====== ======
r-- No No No No
-w- No No No No
–x Yes No Yes No
r-x Yes Yes Yes No
rwx Yes Yes Yes Yes
====== ====== ====== ====== ======

查看和修改权限

通过 :ref:ls -l <cmd_ls> 可以查看到其文件类型及权限,通过 :ref:chmod <cmd_chmod> 修改权限,通过 :ref:chown <cmd_chown> 改变文件或目录的属主,通过 :ref:chgrp <cmd_chgrp> 改变文件或目录的所属组。

[Linux]$ ls -l /
total 60
lrwxrwxrwx   1 root root     7 Aug  4 15:58 bin -> usr/bin
drwxr-xr-x   3 root root  4096 Aug  4 16:19 boot
drwxr-xr-x   3 root root  4096 Aug  4 16:20 home
...

输出中,第 1 个字符表示文件类型,第 2-10 字符部分表示文件的权限位,共有 9 位。

在这里插入图片描述

进程权限控制信息


对于进程,有如下属性与文件访问权限相关:

  • effective user id:进程访问文件权限相关的 UID(简写为 euid)
  • effective group id:进程访问文件权限相关的 GID(简写为 egid)
  • real user id:创建进程的用户登录系统时的 UID(简写为 ruid)
  • real group id:创建进程的用户登录系统时的 GID(简写为 rgid)
  • saved set user id:当进程被执行时拷贝自 euid
  • saved set group id:当进程被执行时拷贝自 egid

实际用户ID(RUID):用于标识一个系统中用户是谁,一般就是登陆的用户 uid

有效用户ID(EUID):用于系统决定用户对系统资源的权限,一般就是进程的创建者(也就是属主)。

可以通过 top 命令查看进程的 euid 和 ruid。

进程访问文件的控制策略

对于进程访问文件而言,最重要的是 euid, 所以其权限属性均以 euid 为 “中心”。

  • 进程的 euid 一般为其 ruid 值,若文件具有 s 权限,其 euid 为该文件的 user id
  • 进程的 saved set user id 拷贝自 euid.
  • 当进程的 euid 与文件的 user id 匹配时,进程才具有文件 user 权限位所设定的权限
  • 组权限 egid 的控制规则类似。

通过 exec 调用可执行文件之时:

  • 进程 ruid 值始终不变
  • saved set-user ID 始终来自 euid
  • euid 值取决于文件的 set-user-ID 位是否被设置

假设 man 程序文件被用户 man 所拥有,并且已经被设置了 set-user-ID 位,当用执行它的时候,会有如下情况:

  • real user ID= 我们的用户 UID
  • effective user ID= man 用户 UID
  • saved set-user-ID= man 用户 UID

man 程序会访问需要的配置文件,这些文件由 man 用户所拥有,但是由于 effective user ID 是 man,文件的访问就被允许了。


计算机端口
####################################

简介


计算机“端口”是英文 port 的义译,可以认为是计算机与外界通讯交流的出口。其中硬件领域的端口又称接口,如:USB端口、串行端口等。软件领域的端口一般指网络中面向连接服务和无连接服务的通信协议端口,是一种抽象的软件结构,包括一些数据结构和 I/O(基本输入输出)缓冲区。

从现实世界理解端口

将计算机想象成一栋医院大楼,里边有很多科室,科室里还会有主任医师、副主任医师、主治医师等等的单独诊室。挂号处就相当于 inetd 守护进程,而一个个的诊室就相当于端口(门牌号相当于端口号),每个端口负责不同的服务,处理不同的请求。比如:80 诊室负责处理 Web 请求,访客可以通过 80 端口获得网页内容。后来负责处理 Web 请求的人搬进了 8080 诊室,继续做相同的工作。另外一个人搬进了 80 诊室,但他并不处理 Web 请求(或者 80 诊室停止使用了)。那么再有试图通过 80 诊室获得网页内容的访客就会“吃闭门羹”,只有通过新的 8080 诊室才能获得网页内容。

每个端口都拥有一个叫端口号的整数描述符,用来区别不同的端口。由于 TCP/IP 传输层的 TCP 和 UDP 两个协议是两个完全独立的软件模块,因此各自的端口号也相互独立。如 TCP 有一个 255 号端口,UDP 也可以有一个 255 号端口,两者并不冲突。

面向连接(TCP)和无连接协议(UDP)

面向连接服务的特点:面向连接服务要经过三个阶段:数据传数前,先建立连接;连接建立后再传输数据;数据传送完后,释放连接。面向连接服务,可确保数据传送的次序和传输的可靠性。

无连接服务的特点是:无连接服务只有传输数据阶段,消除了除数据通信外的其它开销。它的优点是灵活方便、迅速,特别适合于传送少量零星的报文。缺点也很明显,它不能防止报文的丢失、重复或失序。

要区分“面向连接服务”和“无连接服务”的概念特别简单,来举两个形象的例子:打电话和写信。两个人要通电话,必须先建立连接——拨号,等待应答后才能相互传递信息,最后还要释放连接——挂电话。而写信就没有那么复杂了,地址姓名填好以后直接往邮筒一扔,收信人就能收到。TCP/IP 协议里面低于 1024 的端口都有确切的定义,它们对应着因特网上常见的一些服务。这些常见的服务可以划分为使用 TCP 端口(面向连接如打电话)和使用 UDP 端口(无连接如写信)两种。

端口号有两种基本分配方式:第一种叫全局分配,这是一种集中分配方式,由一个公认权威的中央机构根据用户需要进行统一分配,并将结果公布于众;第二种是本地分配(又称动态连接),即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回本地唯一的端口号,进程再通过合适的系统调用,将自己和该端口连接起来(binding,绑定)。

TCP/IP 端口号的分配综合了以上两种方式,将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。每一个标准服务器都拥有一个全局公认的端口叫周知口,即使在不同的机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP 和 UDP 规定,小于 256 的端口才能作为保留端口。

按端口号可分为三大类:

  1. 公认端口(Well Known Ports):从 0 到 1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80 端口实际上总是 HTTP 通讯。

  2. 注册端口(Registered Ports):从 1024 到 49151,它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从 1024 左右开始。

  3. 动态/私有端口(Dynamic and/or Private Ports):从 49152 到 65535,理论上,不应为服务分配这些端口。实际上,机器通常从 1024 起分配动态端口。但也有例外:SUN 的 RPC 端口从 32768 开始。

系统管理员可以“重定向”端口:

实现重定向是为了隐藏公认的默认端口,降低受破坏率。如果有人要对一个默认端口进行攻击必须先进行端口扫描,确定是否开启了此端口。大多数端口重定向与原端口有相似之处,例如多数 HTTP 端口由 80 变化而来:81、88、8000、8080、8888,许多人有其它原因选择奇怪的数:42,69,666,31337。Blake R. Swopes 指出使用重定向端口还有一个原因,在 UNIX 系统上,想侦听 1024 以下的端口需要有 root 权限,如果你没有 root 权限而又想开 web 服务,就需要将其重定向到较高的端口。此外,一些 ISP 的防火墙(路由器层面)将阻挡低端口的通讯,这样的话即使你拥有主机 root 权限,需要连接外网还是得重定向到较高的端口。

将默认的 HTTP 端口 80 重定向到端口 8080,网页的访问地址也要相应的改变:http://wwd.adiba.top:8080/net/port.html。

关闭不必要的端口


有人曾经把服务器比作房子,而把端口比作通向不同房间(服务)的门,如果不考虑细节的话,这是一个不错的比喻。入侵者要进入房子,势必要破门窗而入,那么对于入侵者来说,了解房子开了几扇门,都是什么样的门,门后面有什么东西就显得至关重要。

入侵者通常会用扫描器对目标主机的端口进行扫描,以确定哪些端口是开放的,从开放的端口,入侵者可以知道目标主机大致提供了哪些服务,进而猜测可能存在的漏洞,因此对端口的扫描可以帮助我们更好的了解目标主机,而对于管理员,扫描本机的开放端口也是做好安全防范的第一步。

查看端口信息

netstat -a     #列出所有端口
netstat -at    #列出所有tcp端口
netstat -au    #列出所有udp端口

netstat -l        #只显示监听端口
netstat -lt       #只列出所有监听 tcp 端口
netstat -lu       #只列出所有监听 udp 端口

关闭一个端口

下面介绍两种关闭 linux 系统端口的方法:

  1. 通过杀掉进程的方法来关闭端口

    每个端口都是一个进程占用着,都会有一个守护进程,kill 掉这个守护进程就可以了。

    netstat -anp | grep 端口号 # 找出占用端口的进程
    kill -9 PID # 通过 PID 杀掉进程
    
  2. 通过防火墙限制端口

    其中 $port 即为端口数字,iptables 的具体用法可以查看 man 手册

    iptables -A INPUT -p $port -j ACCEPT # 打开端口服务
    iptables -A INPUT -p $port -j DROP   # 关闭端口服务
    

特殊的文件


/etc/services 文件是记录网络服务名和它们对应使用的端口号及协议。文件中的每一行对应一种服务,它由 4 个字段组成,中间用 TAB 或空格分隔,分别表示“服务名称”、“使用端口”、“协议名称”以及“别名”。

# /etc/services 文件截取部分
kermit           1649/udp
l2tp             1701/tcp     l2f
l2tp             1701/udp     l2f
h323gatedisc     1718/tcp

很多的系统程序都要使用这个文件。如果每一个服务都能够严格遵循该机制,在此文件里标注自己所使用的端口信息,则主机上各服务间对端口的使用,将会非常清晰明了,易于管理;在该文件中定义的服务名,可以作为配置文件中的参数使用。例如:在配置路由策略时,使用"www"代替"80",即为调用了此文件中的条目“www 80”;

且当有特殊情况,需要调整端口设置,只需要在 /etc/services 中修改 www 的定义,即可影响到服务。

例如:在文件中增加条目“privPort 55555”,在某个私有服务中多个配置文件里广泛应用,进行配置。当有特殊需要,要将这些端口配置改为66666,则只需修改 /etc/services 文件中对应行即可。

在应用程序中可以通过服务名和协议获取到对应的端口号,通过在该文件注册可以使应用程序不再关心端口号。


硬盘分区的 UUID
####################################

UUID 是指通用唯一识别码(Universally Unique Identifier),用于帮助 Linux 系统识别一个磁盘分区而不是块设备文件。UUID 在系统中使用一个 128 位(比特)的数字来标识。

UUID 最初被用在阿波罗网络计算机系统(NCS)中,之后 UUID 被开放软件基金会(OSF)标准化,成为分布式计算环境(DCE)的一部分。自 Linux 内核 2.15.1 起,libuuid 就是 util-linux-ng 包中的一部分,它被默认安装在 Linux 系统中,UUID 由该库生成,可以认为在一个系统中 UUID 是唯一的,并且在所有系统中也是唯一的。

UUID 以 32 个十六进制的数字表示,被连字符分割为 5 组(格式为 8-4-4-4-12)。现在大多数的 Linux 系统都使用 UUID 挂载分区,这一点可以在 /etc/fstab 文件中验证:

# cat /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a device; this may
# be used with UUID= as a more robust way to name devices that works even if
# disks are added and removed. See fstab(5).
#
#
UUID=69d9dd18-36be-4631-9ebb-78f05fe3217f / ext4 defaults,noatime 0 1
UUID=a2092b92-af29-4760-8e68-7a201922573b swap swap defaults,noatime 0 2

查看 UUID


在查看硬盘的 UUID 之前,最好先 :ref:mount <cmd_mount> 挂载硬盘。

查看 by-uuid 文件夹

Linux 中 /dev/disk/by-uuid/ 目录包含了 UUID 与实际的块设备文件链接。

[Linux]$ ls -lh /dev/disk/by-uuid/
total 0
lrwxrwxrwx 1 root root 10 Jan 29 08:34 ca307aa4-0866-49b1-8184-004025789e63 -> ../../sdc3
lrwxrwxrwx 1 root root 10 Jan 29 08:34 d17e3c31-e2c9-4f11-809c-94a549bc43b7 -> ../../sdc1
lrwxrwxrwx 1 root root 10 Jan 29 08:34 d92fa769-e00f-4fd7-b6ed-ecf7224af7fa -> ../../sda1

lsblk 命令

使用 :ref:lsblk <cmd_lsblk> 命令列出所有有关可用或指定块设备的信息。lsblk 命令读取 sysfs 文件系统和 udev 数据库以收集信息。

如果 udev 数据库不可用或者编译的 lsblk 不支持 udev,它会试图从块设备中读取卷标、UUID 和文件系统类型。这种情况下,必须以 root 身份运行。该命令默认会以类似于树的格式打印出所有的块设备(RAM 盘除外)。

[Linux]# lsblk -o name,mountpoint,size,uuid
NAME   MOUNTPOINT  SIZE UUID
sda                 30G 
└─sda1 /            20G d92fa769-e00f-4fd7-b6ed-ecf7224af7fa
sdb                 10G 
sdc                 10G 
├─sdc1               1G d17e3c31-e2c9-4f11-809c-94a549bc43b7
├─sdc3               1G ca307aa4-0866-49b1-8184-004025789e63
├─sdc4               1K 
└─sdc5               1G 
sdd                 10G 
sde                 10G 
sr0               1024M 

blkid 命令

:ref:blkid <cmd_blkid> 是定位或打印块设备属性的命令行实用工具。它利用 libblkid 库在 Linux 系统中获得到磁盘分区的 UUID。

[Linux]$ blkid
/dev/sda1: UUID="d92fa769-e00f-4fd7-b6ed-ecf7224af7fa" TYPE="ext4" PARTUUID="eab59449-01"
/dev/sdc1: UUID="d17e3c31-e2c9-4f11-809c-94a549bc43b7" TYPE="ext2" PARTUUID="8cc8f9e5-01"
/dev/sdc3: UUID="ca307aa4-0866-49b1-8184-004025789e63" TYPE="ext4" PARTUUID="8cc8f9e5-03"
/dev/sdc5: PARTUUID="8cc8f9e5-05"

终端、控制台和 Shell 的区别
####################################

在网上看一些教程或文档时,经常会看到”打开终端输入命令“、”打开 Shell 输入命令“、“在控制台中输入命令”等等许多和系统交互的方式,但是在 Linux 中,控制台、终端和 Shell 又傻傻的分不清楚。

  • 终端(terminal 简称为 term)
  • 控制台(console)
  • Shell 俗称壳(用来区别于核)

从历史说起


在计算机发展的最初,有两个最主要的特点:个头大和价格昂贵。在 20 世纪 70 年代 Ken Thompson 在 PDP-11(DEC 公司制造的小型计算机) 上开发 UNIX 系统时,为了解决计算机价格昂贵的问题,他们把 UNIX 设计成了多任务、多用户的操作系统。但是在那个年代所有的机器都非常昂贵,还是为了解决钱的问题,他们选择了一个价格便宜并且可用的机器( Teletype ASR33)来连接到 PDP-11,使计算机可以让多个人使用。

最初开发 Teletype ASR33(“Teletype” 是一个商标名称。ASR 代表自动发送与接收,即 Automatic Send-Receive)的目的是通过电话线发送和接收消息,所以该机器被称为电传打字机(Teletypewriter 缩写为 TTY)。

在这里插入图片描述

在这里插入图片描述

所有的 Teletype 都有一个键盘用于输入和一卷纸用于打印输出。为了存储和读取数据还自带了一个纸带穿孔机和纸带阅读机。它没有屏幕、没有鼠标,也没有声音,但是它经济实惠并且可用。

在 UNIX 系统中,将 Teletype ASR33 称为终端(terminal),而将 PDP-11 称为主机(host)。其中终端只有两个功能:接受输入和打印输出。

在所有连接到主机的终端中,有一台终端比较特殊。可以把它看成是主机的一部分,它是用来管理系统的,这台特殊的终端就是控制台(console)。一台主机只有一个控制台。在启动计算机的时候,所有的信息都会显示到控制台上。在操作计算机的过程中,与终端不相关的信息,比如内核消息,后台服务消息,也会显示到控制台上。

简单来说,控制台是计算机的基本设备,而终端是附加设备。

来感受一下最初的 UNIX 计算机,PDP-11 主机和多个 Teletype ASR33 终端。

在这里插入图片描述

看视频感受一下当时的程序员是怎么工作的。

`点击查看视频 >> <http://data.dongxg.top/teletype_ASR33.mp4>`_

随着时代的发展,UNIX 终端也越来越先进。键盘越来越完善并且更方便易用,纸上打印的输出方式也被屏幕显示器所取代。在历史上曾经出过数百个不同类型的终端,由于生产厂家不同,所遵循的标准不同,因此有不同的类型和标准(混乱的年代,还好没赶上)。但是其中有一种最流行的终端 VT100(1978 年由 DEC 公司生产),它是如此的流行,以至于被设立为一种永久的标准,大多数终端仿真程序都使用基于 VT100 的规范。

在这里插入图片描述

说完了硬件,在来看一下软件 Shell,Shell俗称壳,是读取并解释命令的程序。Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁,用户通过 Shell 访问操作系统内核的服务。常用的几种 shell 解释器:sh、bash、zsh、ash、csh。

虚拟化


由于时代的发展计算机的硬件越来越便宜,现在都是一个人独占一台计算机(个人电脑),已经不再需要“终端”。因此,终端和控制台由硬件的概念慢慢演化成了软件的概念,终端和控制台的界限也慢慢模糊了。

虽然没有了“硬件终端”,但是在终端或 TTY 硬件接插的地方,操作系统仍然需要一个程序来监视串行端口。getty 就是一个监视串行端口的程序,对一台计算机来说,一个伪装的 TTY(Pseudo-TTY,也叫做“PTY”)就是一个终端。当运行一个 GNOME 终端程序时,就像是连接上了一个“硬件终端”。

现在说的终端,都是软件的概念,是用软件来模拟以前硬件的工作方式。比如在 linux 操作系统中,用 alt+f1~f6 可以切换 6 个虚拟终端,就好像是以前多人共用计算机中的终端设备。当然,你也可以通过串口线连接一个真正的终端,只是这种终端设备已经非常罕见了。

在 linux 操作系统中,控制台和终端的区别越来越模糊。比如下面这条命令: ``echo "hello,world" > /dev/console`` 将 ``"hello,world"`` 显示到控制台上( ``/dev/console`` 是控制台的设备名)。在字符模式下,无论在哪个虚拟终端下执行这条命令,字符 ``hello,world`` 都会显示在当前的虚拟终端下,也就是说,linux 把当前的终端当作控制台来看待。

但是在 UNIX 系统中,却很明显的有虚拟终端和控制台的区别。比如在 freeBSD 系统中,只有第一个“终端”才是真正的控制台(就是说按 alt+f1 得到的那个虚拟终端)。无论在哪个虚拟终端上执行 ``echo "hello,world" > /dev/console`` 命令(哪怕是通过网络连接的伪终端上执行这条命令), ``hello,world`` 字符只会显示到控制台上。

普通用户可以简单的把终端和控制台理解为:可以输入命令行并显示程序运行过程中的信息以及运行结果的窗口。不必要严格区分这两者的差别。

tty 和 getty 也是一个 Unix 命令,用来给出当前终端设备的名称。

终端、控制台、Shell 都软件化之后他们的界限很模糊,一般情况下,可以把三者等同。

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码讲故事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值