2. 文件的各种属性

首先通过文件的结构体来看看文件到底有哪些属性:

struct stat {
    dev_t st_dev; /* 设备   */
    ino_t st_ino; /* 节点   */
    mode_t st_mode; /* 模式   */
    nlink_t st_nlink; /* 硬连接 */
    uid_t st_uid; /* 用户ID */
    gid_t st_gid; /* 组ID   */
    dev_t st_rdev; /* 设备类型 */
    off_t st_off; /* 文件字节数 */
    unsigned long  st_blksize; /* 块大小 */
    unsigned long st_blocks; /* 块数   */
    time_t st_atime; /* 最后一次访问时间 */
    time_t st_mtime; /* 最后一次修改时间 */
    time_t st_ctime; /* 最后一次改变时间(指属性) */
};

下面逐次来了解这些属性,如果需要查看某个文件属性,用 stat 命令就可,它会按照上面的结构体把信息列出来。另外,ls 命令在跟上一定参数后也可以显示文件的相关属性,比如 -l 参数。

文件类型

文件类型对应于上面的 st_mode, 文件类型有很多,比如常规文件、符号链接(硬链接、软链接)、管道文件、设备文件(符号设备、块设备)、socket文件等,不同的文件类型对应不同的功能和作用。

范例:在命令行简单地区分各类文件

$ ls -l
total 12
drwxr-xr-x 2 root root 4096 2007-12-07 20:08 directory_file
prw-r--r-- 1 root root    0 2007-12-07 20:18 fifo_pipe
brw-r--r-- 1 root root 3, 1 2007-12-07 21:44 hda1_block_dev_file
crw-r--r-- 1 root root 1, 3 2007-12-07 21:43 null_char_dev_file
-rw-r--r-- 2 root root  506 2007-12-07 21:55 regular_file
-rw-r--r-- 2 root root  506 2007-12-07 21:55 regular_file_hard_link
lrwxrwxrwx 1 root root   12 2007-12-07 20:15 regular_file_soft_link -> regular_file
$ stat directory_file/
  File: `directory_file/'
  Size: 4096            Blocks: 8          IO Block: 4096   directory
Device: 301h/769d       Inode: 521521      Links: 2
Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2007-12-07 20:08:18.000000000 +0800
Modify: 2007-12-07 20:08:18.000000000 +0800
Change: 2007-12-07 20:08:18.000000000 +0800
$ stat null_char_dev_file
  File: `null_char_dev_file'
  Size: 0               Blocks: 0          IO Block: 4096   character special file
Device: 301h/769d       Inode: 521240      Links: 1     Device type: 1,3
Access: (0644/crw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2007-12-07 21:43:38.000000000 +0800
Modify: 2007-12-07 21:43:38.000000000 +0800
Change: 2007-12-07 21:43:38.000000000 +0800

说明:通过 ls 命令结果每行的第一个字符可以看到,它们之间都不相同,这正好反应了不同文件的类型。 d 表示目录,- 表示普通文件(或者硬链接),l 表示符号链接,p 表示管道文件,bc 分别表示块设备和字符设备(另外 s 表示 socket 文件)。在 stat 命令的结果中,可以在第二行的最后找到说明,从上面的操作可以看出,directory_file 是目录,stat 命令的结果中用 directory 表示,而 null_char_dev_file 它则用 character special file 说明。

范例:简单比较它们的异同

通常只会用到目录、普通文件、以及符号链接,很少碰到其他类型的文件,不过这些文件还是各有用处的,如果要做嵌入式开发或者进程通信等,可能会涉及到设备文件、有名管道(FIFO)。下面通过简单的操作来反应它们之间的区别(具体原理会在下一节 第六章 文件系统操作 介绍,如果感兴趣,也可以提前到网上找找设备文件的作用、块设备和字符设备的区别、以及驱动程序中如何编写相关设备驱动等)。

对于普通文件:就是一系列字符的集合,所以可以读、写等

$ echo "hello, world" > regular_file
$ cat regular_file
hello, world

在目录中可以创建新文件,所以目录还有叫法:文件夹,到后面会分析目录文件的结构体,它实际上存放了它下面的各个文件的文件名。

$ cd directory_file
$ touch file1 file2 file3

对于有名管道,操作起来比较有意思:如果要读它,除非有内容,否则阻塞;如果要写它,除非有人来读,否则阻塞。它常用于进程通信中。可以打开两个终端 terminal1terminal2,试试看:

terminal1$ cat fifo_pipe #刚开始阻塞在这里,直到下面的写动作发生,才打印test字符串
terminal2$ echo "test" > fifo_pipe

关于块设备,字符设备,设备文件对应于 /dev/hda1/dev/null,如果用过 U 盘,或者是写过简单的脚本的话,这样的用法应该用过: :-)

$ mount hda1_block_dev_file /mnt #挂载硬盘的第一个分区到/mnt下(关于挂载的原理,在下一节讨论)
$ echo "fewfewfef" > /dev/null   #/dev/null像个黑洞,什么东西丢进去都消失殆尽

最后两个文件分别是 regular_file 文件的硬链接和软链接,去读写它们,他们的内容是相同的,不过去删除它们,他们却互不相干,硬链接和软链接又有何不同呢?前者可以说就是原文件,后者呢只是有那么一个 inode,但没有实际的存储空间,建议用 stat 命令查看它们之间的区别,包括它们的 Blocksinode 等值,也可以考虑用 diff 比较它们的大小。

$ ls regular_file*
ls regular_file* -l
-rw-r--r-- 2 root root 204800 2007-12-07 22:30 regular_file
-rw-r--r-- 2 root root 204800 2007-12-07 22:30 regular_file_hard_link
lrwxrwxrwx 1 root root     12 2007-12-07 20:15 regular_file_soft_link -> regular_file
$ rm regular_file      # 删除原文件
$ cat regular_file_hard_link   # 硬链接还在,而且里头的内容还有呢
fefe
$ cat regular_file_soft_link
cat: regular_file_soft_link: No such file or directory

虽然软链接文件本身还在,不过因为它本身不存储内容,所以读不到东西,这就是软链接和硬链接的区别。

需要注意的是,硬链接不可以跨文件系统,而软链接则可以。另外,也不允许给目录创建硬链接。

范例:普通文件再分类

文件类型从 Linux 文件系统那么一个级别分了以上那么多类型,不过普通文件还是可以再分的(根据文件内容的”数据结构“分),比如常见的文本文件,可执行的 ELF 文件,odt 文档,jpg 图片格式,swap 分区文件,pdf 文件。除了文本文件外,它们大多是二进制文件,有特定的结构,因此需要有专门的工具来创建和编辑它们。关于各类文件的格式,可以参考相关文档标准。不过非常值得深入了解 Linux 下可执行的 ELF 文件的工作原理,如果有兴趣,建议阅读一下参考资料中和 ELF 文件相关部分,这一部分对于嵌入式 Linux 工程师至关重要。

虽然各类普通文件都有专属的操作工具,但是还是可以直接读、写它们,这里先提到这么几个工具,回头讨论细节。

  • od :以八进制或者其他格式“导出”文件内容。
  • strings :读出文件中的字符(可打印的字符)
  • gccgdbreadelf,objdump等:ELF文件分析、处理工具(gcc编译器、gdb调试器、readelf分析 ELF 文件,objdump` 反编译工具)

再补充一个非常重要的命令,file,这个命令用来查看各类文件的属性。和 stat 命令相比,它可以进一步识别普通文件,即 stat 命令显示的 regular file 。因为 regular file 可以有各种不同的结构,因此在操作系统的支持下得到不同的解释,执行不同的动作。虽然,Linux 下,文件也会加上特定的后缀以便用户能够方便地识别文件的类型,但是 Linux 操作系统根据文件头识别各类文件,而不是文件后缀,这样在解释相应的文件时就更不容易出错。下面简单介绍 file 命令的用法。

$ file ./
./: directory
$ file /etc/profile
/etc/profile: ASCII English text
$ file /lib/libc-2.5.so
/lib/libc-2.5.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped
$ file /bin/test
/bin/test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), stripped
$ file /dev/hda
/dev/hda: block special (3/0)
$ file /dev/console
/dev/console: character special (5/1)
$ cp /etc/profile .
$ tar zcf profile.tar.gz profile
$ file profile.tar.gz
profile.tar.gz: gzip compressed data, from Unix, last modified: Tue Jan  4 18:53:53 2000
$ mkfifo fifo_test
$ file fifo_test
fifo_test: fifo (named pipe)

更多用法见 file 命令的手册,关于 file 命令的实现原理,请参考 magic 的手册(看看 /etc/file/magic 文件,了解什么是文件的 magic number 等)。

文件属主

Linux 作为一个多用户系统,为多用户使用同一个系统提供了极大的方便,比如对于系统上的文件,它通过属主来区分不同的用户,以便分配它们对不同文件的操作权限。为了更方便地管理,文件属主包括该文件所属用户,以及该文件所属的用户组,因为用户可以属于多个组。先来简单介绍 Linux 下用户和组的管理。

Linux 下提供了一组命令用于管理用户和组,比如用于创建用户的 useraddgroupadd,用于删除用户的 userdelgroupdel,另外,passwd 命令用于修改用户密码。当然,Linux 还提供了两个相应的配置,即 /etc/passwd/etc/group,另外,有些系统还把密码单独放到了配置文件 /etc/shadow 中。关于它们的详细用法请参考后面的资料,这里不再介绍,仅介绍文件和用户之间的一些关系。

范例:修改文件的属主

$ chown 用户名:组名 文件名

如果要递归地修改某个目录下所有文件的属主,可以添加 -R 选项。

从本节开头列出的文件结构体中,可以看到仅仅有用户 ID 和组 ID 的信息,但 ls -l 的结果却显示了用户名和组名信息,这个是怎么实现的呢?下面先看看 -n 的结果:

范例:查看文件的属主

$ ls -n regular_file
-rw-r--r-- 1 0 0 115 2007-12-07 23:45 regular_file
$ ls -l regular_file
-rw-r--r-- 1 root root 115 2007-12-07 23:45 regular_file

范例:分析文件属主实现的背后原理

可以看到,ls -n 显示了用户 ID 和组 ID,而 ls -l 显示了它们的名字。还记得上面提到的两个配置文件 /etc/passwd/etc/group 文件么?它们分别存放了用户 ID 和用户名,组 ID 和组名的对应关系,因此很容易想到 ls -l 命令在实现时是如何通过文件结构体的 ID 信息找到它们对应的名字信息的。如果想对 ls -l 命令的实现有更进一步的了解,可以用 strace 跟踪看看它是否读取了这两个配置文件。

$ strace -f -o strace.log ls -l regular_file
$ cat strace.log | egrep "passwd|group|shadow"
2989  open("/etc/passwd", O_RDONLY)     = 3
2989  open("/etc/group", O_RDONLY)      = 3

说明: strace 可以用来跟踪系统调用和信号。如同 gdb 等其他强大的工具一样,它基于系统的 ptrace 系统调用实现。

实际上,把属主和权限分开介绍不太好,因为只有它们两者结合才使得多用户系统成为可能,否则无法隔离不同用户对某个文件的操作,所以下面来介绍文件操作权限。

文件权限

ls -l 命令的结果的第一列的后 9 个字符中,可以看到类似这样的信息 rwxr-xr-x,它们对应于文件结构体的 st_mode 部分(st_mode 包含文件类型信息和文件权限信息两部分)。这类信息可以分成三部分,即 rwxr-xr-x,分别对应该文件所属用户、所属组、其他组对该文件的操作权限,如果有 rwx 中任何一个表示可读、可写、可执行,如果为 - 表示没有这个权限。对应地,可以用八进制来表示它,比如 rwxr-xr-x 就可表示成二进制 111101101,对应的八进制则为 755 。正因为如此,要修改文件的操作权限,也可以有多种方式来实现,它们都可通过 chmod 命令来修改。

范例:给文件添加读、写、可执行权限

比如,把 regular_file 的文件权限修改为所有用户都可读、可写、可执行,即 rwxrwxrwx,也可表示为 111111111,翻译成八进制,则为 777 。这样就可以通过两种方式修改这个权限。

$ chmod a+rwx regular_file
或
$ chmod 777 regular_file

说明: a 指所有用户,如果只想给用户本身可读可写可执行权限,那么可以把 a 换成 u ;而 + 就是添加权限,相反的,如果想去掉某个权限,用 -,而 rwx 则对应可读、可写、可执行。更多用法见 chmod 命令的帮助。

实际上除了这些权限外,还有两个涉及到安全方面的权限,即 setuid/setgid 和只读控制等。

如果设置了文件(程序或者命令)的 setuid/setgid 权限,那么用户将可用 root 身份去执行该文件,因此,这将可能带来安全隐患;如果设置了文件的只读权限,那么用户将仅仅对该文件将有可读权限,这为避免诸如 rm -rf 的“可恶”操作带来一定的庇佑。

范例:授权普通用户执行root所属命令

默认情况下,系统是不允许普通用户执行 passwd 命令的,通过 setuid/setgid,可以授权普通用户执行它。

$ ls -l /usr/bin/passwd
-rwx--x--x 1 root root 36092 2007-06-19 14:59 /usr/bin/passwd
$ su      #切换到root用户,给程序或者命令添加“粘着位”
$ chmod +s /usr/bin/passwd
$ ls -l /usr/bin/passwd
-rws--s--x 1 root root 36092 2007-06-19 14:59 /usr/bin/passwd
$ exit
$ passwd #普通用户通过执行该命令,修改自己的密码

setuidsetgid 位是让普通用户可以以 root 用户的角色运行只有 root 帐号才能运行的程序或命令。

虽然这在一定程度上为管理提供了方便,比如上面的操作让普通用户可以修改自己的帐号,而不是要 root 帐号去为每个用户做这些工作。关于 setuid/setgid 的更多详细解释,请参考最后推荐的资料。

范例:给重要文件加锁

只读权限示例:给重要文件加锁(添加不可修改位 [immutable])),以避免各种误操作带来的灾难性后果(例如 : rm -rf

$ chattr +i regular_file
$ lsattr regular_file
----i-------- regular_file
$ rm regular_file    #加immutable位后就无法对文件进行任何“破坏性”的活动啦
rm: remove write-protected regular file `regular_file'? y
rm: cannot remove `regular_file': Operation not permitted
$ chattr -i regular_file #如果想对它进行常规操作,那么可以把这个位去掉
$ rm regular_file

说明: chattr 可以用于设置文件的特殊权限,更多用法请参考 chattr 的帮助。

文件大小

文件大小对于普通文件而言就是文件内容的大小,而目录作为一个特殊的文件,它存放的内容是以目录结构体组织的各类文件信息,所以目录的大小一般都是固定的,它存放的文件个数自然也就有上限,即它的大小除以文件名的长度。设备文件的“文件大小”则对应设备的主、次设备号,而有名管道文件因为特殊的读写性质,所以大小常是 0 。硬链接(目录文件不能创建硬链接)实质上是原文件的一个完整的拷贝,因此,它的大小就是原文件的大小。而软链接只是一个 inode,存放了一个指向原文件的指针,因此它的大小仅仅是原文件名的字节数。下面我们通过演示增加记忆。

范例:查看普通文件和链接文件

文件,链接文件文件大小的示例:

$ echo -n "abcde" > regular_file   #往regular_file写入5字节
$ ls -l regular_file*
-rw-r--r-- 2 root root  5 2007-12-08 15:28 regular_file
-rw-r--r-- 2 root root  5 2007-12-08 15:28 regular_file_hard_file
lrwxrwxrwx 1 root root 12 2007-12-07 20:15 regular_file_soft_link -> regular_file
lrwxrwxrwx 1 root root 22 2007-12-08 15:21 regular_file_soft_link_link -> regular_file_soft_link
$ i="regular_file"
$ j="regular_file_soft_link"
$ echo ${#i} ${#j}   #软链接存放的刚好是它们指向的原文件的文件名的字节数
12 22

范例:查看设备文件

设备号对应的文件大小:主、次设备号

$ ls -l hda1_block_dev_file
brw-r--r-- 1 root root 3, 1 2007-12-07 21:44 hda1_block_dev_file
$ ls -l null_char_dev_file
crw-r--r-- 1 root root 1, 3 2007-12-07 21:43 null_char_dev_file

补充:主 (major)、次(minor)设备号的作用有不同。当一个设备文件被打开时,内核会根据主设备号(major number)去查找在内核中已经以主设备号注册的驱动(可以 cat /proc/devices 查看已经注册的驱动号和主设备号的对应情况),而次设备号(minor number)则是通过内核传递给了驱动本身(参考《The Linux Primer》第十章)。因此,对于内核而言,通过主设备号就可以找到对应的驱动去识别某个设备,而对于驱动而言,为了能够更复杂地访问设备,比如访问设备的不同部分(如硬件通过分区分成不同部分,而出现 hda1hda2hda3 等),比如产生不同要求的随机数(如 /dev/random/dev/urandom 等)。

范例:查看目录

目录文件的大小,为什么是这样呢?看看下面的目录结构体的大小,目录文件的 Block 中存放了该目录下所有文件名的入口。

$ ls -ld directory_file/
drwxr-xr-x 2 root root 4096 2007-12-07 23:14 directory_file/

目录的结构体如下:

struct dirent {
    long d_ino;
    off_t d_off;
    unsigned short d_reclen;
    char d_name[NAME_MAX+1]; /* 文件名称 */
}

文件访问、更新、修改时间

文件的时间属性可以记录用户对文件的操作信息,在系统管理、判断文件版本信息等情况下将为管理员提供参考。因此,在阅读文件时,建议用 cat 等阅读工具,不要用编辑工具 vim 去阅读,因为即使没有做任何修改操作,一旦执行了保存命令,将修改文件的时间戳信息。

文件名

文件名并没有存放在文件结构体内,而是存放在它所在的目录结构体中。所以,在目录的同一级别中,文件名必须是唯一的。