在 UNIX 族的操作系统中,文件系统占有十分重要的地位,文件的概念涵盖了 UNIX 设备和操作对象的全部内容,对设备的操作方式几乎可以与对普通文件的操作等价。本博客对文件系统进行简单的介绍,主要包括如下内容:
█ Linux 下文件的内涵;
█ Linux 下的文件系统布局和文件系统的树形结构;
█ Linux 下的普通文件和设备文件;
█ Linux 下虚拟文件系统的含义;
█ 文件的常用操作方法、文件句柄的含义、open() 函数、close() 函数、read() 函数和 write() 函数的使用及简单实例;
█ 文件操作的高级用法,包含 ioctl() 对特定设备文件进行控制,用 fcntl() 函数控制文件,mmap() 的用法及 fstat() 获得文件的状态值及状态值的含义。
Linux 下的文件系统
文件系统狭义的概念是一种对存储设备上的数据进行组织和控制的机制。在 Linux 下(当然包含 UNIX),文件的含义比较广泛,文件的概念不仅仅包含通常意义的保存在磁盘的各种格式的数据,还包含目录,甚至各种各样的设备,如键盘、鼠标、网卡、标准输出等,引用一句经典的话 “UNIX 下一切皆文件”。
一、Linux 下文件的内涵
Linux 下的文件系统是对复杂系统进行合理抽象的一个经典的例子,它通过一套统一的接口函数对不同的文件进行操作。例如 open() 函数不仅可以打开 ext2 类型的文件,还可以打开 fat32 类型的文件,并且包括如串口设备、显卡等,只不过打开设备的名称不同而己。Linux 下的文件主要分为如下几种。
█ 普通文件:例如保存在磁盘上的 C 文件、可执行文件,以及目录等,这种文件的特性是数据在存储设备上存放,内核提供了对数据的抽象访问,此种文件为一种字节流,访问接口完全独立于在磁盘上的存储数据。
█ 字符设备文件:是一种能够像文件一样被访问的设备,例如控制台、串口等。
█ 块设备文件:磁盘是此类设备文件的典型代表,与普通文件的区别是操作系统对数据的访问进行的重新的格式设计。
█ socket 文件:它是 Linux 中通过网络进行通信的方式,对网络的访问可以通过文件描述符的抽象实现,访问网络和访问普通文件相似。
在 Linux 中用户空间对各种文件的操作是类似的,因为虚拟文件系统 VFS 提供了同一套 API。
二、文件系统的创建
在 Linux 下对磁盘进行操作的工具是 fdisk,与 Windows 下的 fdisk 功能有些类似,但是命令的格式完全不同。
1. 系统分区情况
使用 fdisk 命令査看当前磁盘的情况:
# fdisk -l (列出当前系统的磁盘情况)
Disk /dev/sdb: 1073 MB, 1073741824 bytes (磁盘 sdb 的大小)
255 heads, 63 sectors/track, 130 cylinders, total 2097152 sectors (柱面情况)
Units = sectors of 1 * 512 = 512 bytes (每个单元的情况)
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000
Disk /dev/sdb doesn’t contain a valid partition table (磁盘 sdb 还没有进行分区)
Disk /dev/sda: 32.2 GB, 32212254720 bytes (磁盘 sda 的分区情况)
255 heads, 63 sectors/track, 3916 cylinders, total 62914560 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x000e70b4
Device Boot Start End Blocks Id System
/dev/sdal * 2048 54527999 27262976 83 Linux (sda1 的开始、结束、分区类型)
/dev/sda2 54530046 62912511 4191233 5 Extended (sda2 为扩展分区)
/dev/sda5 54530048 62912511 4191232 82 Linux swap/Solaris (sda5 逻辑分区)
2. 建立分区
可以看到,磁盘 /dev/sdb 没有使用,现在尝试用 fdisk 在没有使用的磁盘 /dev/sdb上进行分区,先查看分区情况,然后建立一个 100M 大小的初级分区,将分区表写入磁盘并退出:
# fdisk /dev/sdb (对 sdb 进行分区)
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel with disk identifier 0x5607bafl.
Changes will remain in memory only, until you decide to write them.
After that, of course, the previous content won't be recoverable.
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
Command (m for help) : p (查看磁盘当前的分区情况)
Disk /dev/sdb: 1073 MB, 1073741824 bytes
255 heads, 63 sectors/track, 130 cylinders, total 2097152 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical)r 512 bytes / 512 bytes
I/O size (minimura/optimal): 512 bytes / 512 bytes
Disk identifier: 0x5607baf1
Device Boot Start End Blocks Id System
Command (m for help) : m (打印命令)
Command action
a toggle a bootable flag (设置为启动分区)
b edit bsd disklabel (编辑 bsd 磁盘)
c toggle the dos compatibility flag (设置 dos 兼容标志)
d delete a partition (删除一个分区)
1 list known partition types (列出当前系统可支持的分区方式)
m print this menu
n add a new partition (增加一个分区)
o create a new empty DOS partition table (建议一个新的 DOS 分区表)
p print the partition table (打印分区情况)
q quit without saving changes (不保存退出)
s create a new empty Sun disklabel (建立一个新的 Sun 空磁盘)
t change a partition's system id (改变分区的 ID)
u change display/entry units (改变显示单元)
v verify the partition table (修正分区表)
w write table to disk and exit (将之前的修改写入磁盘并推出)
x extra functionality (experts only) (专家模式)
Command (m for help): n (建立一个新分区)
Partition type: (选择分区类型)
p primary (0 primary, 0 extended, 4 free) (主分区)
e extended (扩展分区)
Select (default p):p (输入 p,选择违立一个主分区)
Partition number (1-4): 1 (分区的第一个分区)
First sector (2048-2097151, default 2048): (开始扇区,默认为 2048)
Using default value 2048 (按下 Enter 键,选择默认值)
Last sector, +sectors or+size{K/M,G} (2048-2097151, default 2097151) : +100M (建立一个 1OOMbytes 的分区)
Command (m for help) : w (写入磁盘并退出)
The partition table has been altered!
Calling ioctl() to re-read partition table. (调用 ioctl() 函数重读分区进行检査)
Syncing disks. (同步磁盘,将缓存的信息写入磁盘)
3. 查看分区是否成功
列出系统的分区情况,査看上述分区操作是否成功。
# fdisk -l
Disk /dev/sdb: 1073 MB, 1073741824 bytes
224 heads, 19 sectors/track, 492 cylinders, total 2097152 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x4629e5b8
Device Boot Start End Blocks Id System
/dev/sdbl 2048 206847 102400 83 Linux
Disk /dev/sda: 32.2 GB, 32212254720 bytes
255 heads, 63 sectors/track, 3916 cylinders, total 62914560 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x000e70b4
Device Boot Start End Blocks Id System
/dev/sda1 * 2048 54527999 27262976 83 Linux
/dev/sda2 54530046 62912511 4191233 5 Extended
/dev/sda5 54530048 62912511 4191232 82 Linux swap/Solaris
4. 格式化分区
磁盘多一个 sdb1 分区。仅进行分区,分区后的空间并不能使用,需要使用 mkfs 格式化分区 sdb1:
# mkfs.ext4 /dev/sdbl (将 /dev/sdbl 格式化为 ext4 类型的系统)
mke2fs 1.42 (29-Nov-2011)
文件系统标签=
OS type: Linux
块大小=1024 (log=0)
分块大小=1024 (log=0)
Stride=0 blocks, Stripe width=0 blocks
25688 inodes, 102400 blocks
5120 blocks (5.00%) reserved for the super user
第一个数据块=1
Maximum filesystem blocks=67371008
13 block groups
8192 blocks per group, 8192 fragments per group
1976 inodes per group
Superblock backups stored on blocks:
8193, 24577, 40961, 57345, 73729
Allocating group tables:完成
正在写入 inode 表: 完成
Creating journal (4096 blocks): 完成
Writing superblocks and filesystem accounting information: 完成
5. 挂载分区
建立一个 /test 目录,将 sdb1 挂接上去。
# mount /dev/sdb1 /test
6. 查看分区挂载情况
用命令 df 可以査看当前文件系统的情况,例如:
# df
文件系统 1K-块 己用 可用 己用% 挂载点
/dev/sda1 26835196 5602632 19869416 22% /
udev 2053344 4 2053340 1% /dev
tmpfs 824264 808 823456 1% /run
none 5120 0 5120 0% /run/lock
none 2060652 152 2060500 1% /run/shm
/dev/sdb1 99150 5646 88384 7% /test
三、挂接文件系统
Linux 系统下,要使用一个文件系统需要先将文件系统的分区挂载到系统上。mount 命令用于挂载文件,它有很多选项。mount 命令的使用格式为:
mount -t type mountpoint device -o options
上述命令表不将文件类型为 type 的设备 device 挂载到 mountpoint 上,挂载时要遵循 options 的设置。
进行分区挂载时经常使用的是 -t 选项。例如,‘‘-t vfat” 表示挂载 Windows 下的 fat32 等文件类型:“-t proc” 则表示挂载 proc 文件类型。
挂载命令的 -o 选项是一个重量级的设置,经常用于挂载比较特殊的文件属性。在嵌入式 Linux 下,根文件系统经常是不可写的,要对其中的文件进行修改,需要使用 -o 选项重新进行挂载。例如,“-o rewrite,rw” 将文件系统重新进行挂载,并将其属性改为可读写。
Linux 也支持挂载网络文件系统,例如 NFS 文件系统等,挂载 NFS 文件系统的命令 如下:
mount -t nfs 服务器地址:/目录 挂载点
下面是一个挂载 nfs 文件系统的例子,例如在 IP 地址为 192.168.1.150 的机器做了一个 NFS 服务器,提供 192.168.1.x 网段上的 NFS 服务。可以使用下面的命令来实现:
# showmount -e 192.168.1.150 (査看 NFS 服务器共享的文件文件夹)
Export list for 192.168.1.150:
/opt/nfsroot (位于 192.168.1.150 机器上的 /opt/nfsroot 目录)
# mkdir /mnt/nfsmount (在本地机器建一个目录,作为 NFS 挂栽点)
# mount -t nfs 192.168.1.150: /opt/nfsroot /mnt/nfsmount (挂载 NFS)
# df -h (査看本地机挂载 NFS 是不是成功了)
文件系统 容量 已用 可用 己用% 挂载点
/dev/sdal 26G 5.4G 19G 22% /
udev 2.OG 4.OK 2.OG 1% /dev
tmpfs 805M 804K 805M 1% /run
none 5.0M 0 5.OM 0% /run/lock
none 2.0G 152K 2.0G 1% /run/shm
/dev/sdbl 97M 5.6M 87M 7% /test
192.168.1.4:/opt/sirnfs 63G 47G 17G 74% /mnt/nfsmount
(这是挂载成功后的显示)
四、索引节点 inode
在 Linux 下存储设备或存储设备的某个分区格式化为文件系统后,有两个主要的概念 来描述它,一个是索引节点(inode),另一个是块(Block)。
块是用来存储数据的,索引节点则是用来存储数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。
索引节点为每个文件进行信息索引,所以就有了索引节点的数值。
通过查询索引节点,能够快速地找到对应的文件。这就像一本书,存储设备是一本书的整体,块是书的内容,而索引节点相当于一本书的目录,如果要查询某方面的内容,可以通过查询前面的目录,快速地获得内容的信息,例如位置、大小等。
要査看索引节点的信息,可以使用命令 Is,加上参数 -i。例如,使用 ls 査看 hello.c 的索引节点信息,可知索引节点的值为 1050150。
$ls -li hello.c
1050150 -rw-rw-r-- 1 linux-c linux-c 77 5月 29 14:57 hello.c
在 Linux 的文件系统中,索引节点值是文件的标识,并且这个值是唯一的,两个不同文件的索引节点值是不同的,索引节点值相同的文件它的内容是相同的,仅仅文件名不同。修改两个索引节点值相同的文件中的一个文件,另一个文件的内容也跟着发生改变。例如下面的一个例子,使用命令 In 为文件 hello.c 创建个硬链接,命名其文件名为 hello2.c ,并査看属性的变化情况。
$ls -li hello.c (査看 hello.c 的属性)
1050150 -rw-rw-r-- 1 linux-c linux-c 77 5 月 29 14:57 hello.c
$ In hello.c hello2.c (通过 In 来创建 hello.c 的硬链接文件 hello2.c )
$ Is -li hello* (列出 hello.c 和 hello2.c )
1050150 -rw-rw-r-- 2 linux-c linux-c 77 5月 29 14:57 hello2.c
1050150 -rw-rw-r-- 2 linux-c linux-c 77 5月 29 14:57 hello.c
可以看出,hello.c 在没有创建硬链接文件 hello2.c 的时候,其链接个数是 1(即 -rw-rw-r– 后的那个数值),创建了硬链接 hello2.c 后,这个值变成了 2。也就是说,每次为 hello.c 创建一个新的硬链接文件后,其硬链接个数都会增加 1。
索引节点值相同的文件,二者的关系是互为硬链接。当修改其中一个文件的内容时, 互为硬链接的文件内容也会跟着变化。如果删除互为硬链接关系的某个文件时,其他的文件并不受影响。例如把 hello2.c 删除后,还是一样能看到 hello.c 的内容,并且 hello.c 仍是存在的。这是由于索引节点对于每一个文件有一个引用计数,当创建硬链接的时候,引用计数会增加 1,删除文件的时候引用计数会减 1,当引用计数为 0 的时候,系统会删除此文件。
目录不能创建硬链接,只有文件才能创建硬链接。如果目录也可以创建硬链接,很容 易在系统内部形成真实的环状文件系统,对文件系统的维护造成很大的困难。目录可以使用软链接的方式创建,可使用命令 “ln -s”。
五、普通文件
普通文件是指在硬盘、CD、U盘等存储介质上的数据和文件结构。在本博客中所指的文件系统是一个狭义的概念,仅仅按照普通文件在磁盘中组织方式的不同来区分。
普通文件的概念与 Windows 下面文件的概念是相同的。可以对文件进行打开、读出数据、写入数据、关闭、删除等操作。
在 Linux 下,目录也作为一种普通文件存在。
六、设备文件
Linux 下用设备文件来表示所支持的设备,每个设备文件除了设备名,还有 3 个属性,即类型、主设备号、次设备号。例如,査看 sdbl,可以获得磁盘分区 sdbl 的属性,属性的含义如下:
$ls /dev/sdbl -l
brw-rw---- 1 root disk 8, 17 5 月 30 17:20 /dev/sdbl
█ 设备类型:设备属性的第一个字符是这个设备文件的类型。第一个字符为 c,表明这个设备是一个字符设备文件。第一个字符为 b,表明这个设备是一个块设备文件。 sdbl 的第 1 个字符为 b,可知它是一个块设备文件。
█ 主设备号:每一个设备文件都有一个“主设备号”,使用 ls -l 命令输出的第 5 个字段即为主设备号。主设备号是表示系统存取这个设备的“内核驱动”。驱动程序是 Linux 内核中代码的一部分,其作用是用来控制一种特殊设备的输入输出。大多数的 Linux 操作系统都有多种设备驱动程序;每一个设备文件名中的主设备号就代表这个设备使用的是那个设备驱动程序。lsdev 命令可以列出当前内核中配置的驱动程序和这些驱动程序对应的主设备号。
█ 次设备号:每一个设备文件都有一个次设备号。“次设备号”是一个 24 位的十六进制数字,它定义了这个设备在系统中的物理位置。
█ 设备文件名:设备文件名用于表示设备的名称,它遵循标准的命令方式,使得设备的分辨更容易。
1. 字符设备与块设备
字符类型的设备可以在一次数据读写过程中传送任意大小的数据,多个字符的访问是 通过多次读写来完成的,通常用于访问连续的字符。例如,终端、打印机、moderm 和绘图仪等设备是字符类型设备。
块设备文件可以在一次读写过程中访问固定大小的数据,通过块设备文件进行数据读写的时候,系统先从内存的缓冲区中读写数据,而不是直接与设备进行数据读写,这种访问方式可以大幅度地提高读写性能。块类型设备可以随机地访问数据,而数据的访问时间和数据位于设备中的位置无关。常用的块设备有硬盘、软盘和 CD-ROM 及 RAM 类型磁盘。
2. 设备文件的创建
设备文件是通过 mknod 命令来创建的。其命令格式为:
mknod (OPTION)... NAME TYPE [MAJOR MINOR]
其参数有设备文件名 NAME、操作模式 TYPE、主设备号 MAJOR 及次设备号 MINOR。 主设备号和次设备号两个参数合并成一个 16 位的无符号短整数,高 8 位表示主设备号,低 8 位表示次设备号。可以在 include/Linux/major.h 文件中找到所支持的主设备号。
设备文件通常位于 /dev 目录下,下表(Linux 下的设备名简介)显示了目录 /dev 下的一些设备文件的属性。注意同一主设备号既可以标识字符设备,也可以标识块设备。
一个设备文件通常与一个硬件设备(如硬盘,/dev/hda)相关联,或者与硬件设备的某一物理或逻辑分区(如磁盘分区,/dev/hda2)相关联。但在某些情况下,设备文件不会和任何实际的硬件关联,而是表示一个虚拟的逻辑设备。例如, /dev/null 就是对应于一个 “黑洞” 的设备文件,所有写入这个文件的数据都被简单地丢弃。
设备名 | 设备类型 | 主设备号 | 次设备号 | 说明 |
---|---|---|---|---|
/dev/fd0 | 块设备 | 2 | 0 | 软磁盘 |
/dev/hda | 块设备 | 3 | 0 | 第 1 个 IDE 类型硬盘 |
/dev/hda2 | 块设备 | 3 | 2 | 第 1 个 IDE 硬盘上的第 2 个主分区 |
/dev/hdb | 块设备 | 3 | 64 | 第 2 个 IDE 硬盘 |
/dev/hdb3 | 块设备 | 3 | 67 | 第 2 个 IDE 硬盘上的第 3 个主分区 |
/dev/ttyp0 | 字符设备 | 3 | 0 | 终端 |
/dev/console | 字符设备 | 5 | 1 | 控制台 |
/dev/lpl | 字符设备 | 6 | 1 | 打印机 |
/dev/ttyS0 | 字符设备 | 4 | 64 | 第一个串口 |
/dev/rtc | 字符设备 | 10 | 135 | 实时时钟 |
/dev/null | 字符设备 | 1 | 3 | 空设备 |
3. 设备文件的简单操作
设备描述符 /dev/console 是控制台的文件描述符,可以对其进行操作,例如下面的命令将可能造成系统循环运行,甚至死机。
$cat /dev/console
上面的命令将控制台的输入打印出来。下面的命令向标准输出传入字符串 test,系统 将字符串 test 发给标准输出:
$echo "test">/dev/stdout
嵌入式设备中常用的 Framebuffer 设备是一个字符设备,当系统打开 Framebuffer 设置的时候(通常可以在系统启动的时候,修改启动参数,例如在 kernel 一行增加 vga=0x314 启动一个 800X600 分辨率的帧缓冲设备),运行如下命令,先将 Framebuffer 设备 fb0 的数据写入文件 test.txt 中,然后利用 cat 命令将数据写入帧缓存设备 fb0 :
$cat /dev/fb0 > test.txt (获得帧缓存设备的数据)
$cat test.txt > /dev/fb0 (将数据写入帧缓存设备)
七、ext2文件系统
1. ext2 文件系统
我们知道,一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(例如某种 mkfs 命令)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局的信息。下图(ext2 文件系统)是一个磁盘分区格式化成 ext2 文件系统后的存储布局。
文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如 mke2fs 的 -b 选项可以设定块大小为 1024、2048 或 4096 字节。而上图中**启动块(Boot Block)**的大小是确定的,就是 1KB,启动块是由 PC 标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是 ext2 文件系统的开始,ext2 文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由以下部分组成。
超级块(Super Block): 描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次 mount 的时间等等。超级块在每个块组的开头都有一份拷贝。
块组描述符表(GDT,Group Descriptor Table): 由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是 inode 表,从哪里开始是数据块,空闲的 inode 和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第 0 个块组中的拷贝,当执行 e2fsck 检查文件系统一致性时,第 0 个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第 0 个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。
块位图(Block Bitmap) :一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是 1024 字节,某个文件是 2049 字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个 bit 代表本块组中的一个块,这个 bit 为 1 表示该块已用,这个 bit 为 0 表示该块空闲可用。
为什么用 df 命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用 du 命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用 mke2fs 格式化时默认块大小是 1024 字节,可以用 -b 参数指定块大小,现在设块大小指定为 b 字节,那么一个块可以有 8b 个 bit,这样大小的一个块位图就可以表示 8b 个块的占用情况,因此一个块组最多可以有 8b 个块,如果整个分区有 s 个块,那么就可以有 s/(8b) 个块组。格式化时可以用 -g 参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs 工具会计算出最优的数值。inode 位图(inode Bitmap) 和块位图类似,本身占一个块,其中每个 bit 表示一个 inode 是否空闲可用。
inode 表(inode Table):我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是 ls -l 命令看到的那些信息,这些信息存在 inode 中而不是数据块中。每个文件都有一个 inode,一个块组中的所有 inode 组成了 inode 表。
inode 表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs 格式化工具的默认策略是一个块组有多少个 8KB 就分配多少个 inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个 8KB 就分配多少个 inode。
换句话说,如果平均每个文件的大小是 8KB,当分区存满的时候 inode 表会得到比较充分的利用,数据块也不浪费。
如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候 inode 会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用 mke2fs 的 -i 参数手动指定每多少个字节分配一个 inode。
数据块(Data Block)根据不同的文件类型有以下几种情况,对于常规文件的数据存储在数据块中。
对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l 命令看到的其它信息都保存在该文件的 inode 中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。
对于符号链接,如果目标路径名较短则直接保存在 inode 中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
设备文件、FIFO 和 socket 等特殊文件没有数据块,设备文件的主设备号和次设备号保存在 inode 中。
1.1 目录中记录项文件类型
编码文件类型
0 Unknown //不识别文件
1 Regular file //普通文件
2 Directory //目录文件
3 Character device //字符设备
4 Block device //块设备
5 Named pipe //管道
6 Socket //套接字
7 Symbolic link //符号链接
1.2 数据块寻址
数据块寻址如下图所示:
从上图(数据块寻址)可以看出,索引项 Blocks[13] 指向两级的间接寻址块,最多可表示 (b/4)2+b/4+12 个数据块,对于 1K 的块大小最大可表示 64.26MB 的文件。索引项 Blocks[14] 指向三级的间接寻址块,最多可表示 (b/4)3+(b/4)2+b/4+12 个数据块,对于 1K 的块大小最大可表示 16.06GB 的文件。
可见,这种寻址方式对于访问不超过 12 个数据块的小文件是非常快的,访问小文件中的任意数据只需要两次读盘操作,一次读 inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的 inode 和数据块往往已经被内核缓存了,读大文件的效率也不会太低。
2. stat
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
stat 既有命令也有同名函数,用来获取文件 inode 里主要信息,stat 跟踪符号链接,lstat 不跟踪符号链接
stat 里面时间辨析:atime(最近访问时间);mtime(最近更改时间):指最近修改文件内容的时间;ctime(最近改动时间):指最近改动 inode 的时间。
3. access
#include <unistd.h>
int access(const char *pathname, int mode);
按实际用户 ID 和实际组 ID 测试,跟踪符号链接
参数 mode
R_OK 是否有读权限
W_OK 是否有写权限
X_OK 是否有执行权限
F_OK 测试一个文件是否存在
实际用户 ID: 有效用户 ID:sudo 执行时,有效用户 ID 是 root,实际用户 ID 是 username (用户虚拟机的 ID )。
4. chmod
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
下图为 mode 标志:
5. chown
#include <unistd.h>
int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
chown 使用时必须拥有 root 权限。
6. utime
7. truncate
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
8. link
1. link
功能:创建一个硬链接。
当 rm 删除文件时,只是删除了目录下的记录项和把 inode 硬链接计数减 1,当硬链接计数减为 0 时,才会真正的删除文件。
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
█ 硬链接通常要求位于同一文件系统中,POSIX 允许跨文件系统。
█ 符号链接没有文件系统限制。也就是软链接是可以跨文件系统的。
█ 通常不允许创建目录的硬链接,这样做容易出现死循环;某些 unix 系统下超级用户可以创建目录的硬链接。
█ 创建目录项以及增加硬链接计数应当是一个原子操作。
2. symlink
用来创建一个软链接(符号链接)的函数。
int symlink(const char *oldpath, const char *newpath)
3. readlink
读符号链接所指向的文件名字,不读文件内容。
ssize_t readlink(const char *path, char *buf, size_t bufsiz)
4. unlink
int unlink(const char *pathname)
1. 如果是符号链接,删除符号链接
2. 如果是硬链接,硬链接数减 1,当减为 0 时,释放数据块和 inode
3. 如果文件硬链接数为 0,但有进程已打开该文件,并持有文件描述符,则等该进程关闭该文件时,kernel 才真正
去删除该文件
4. 利用该特性创建临时文件,先 open 或 creat 创建一个文件,马上 unlink 此文件
9. rename
功能:文件重命名
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
10. chdir
功能:改变当前进程的工作目录
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
11. getcwd
功能:获取当前进程的工作目录
#include <unistd.h>
char *getcwd(char *buf, size_t size);
12. pathconf
#include <unistd.h>
long fpathconf(int fd, int name);
long pathconf(char *path, int name);
八、目录操作
1. mkdir
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
2. rmdir
#include <unistd.h>
int rmdir(const char *pathname);
3. opendir/fdopendir
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
4. readdir
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all file system types */
char d_name[256]; /* filename */
};
readdir 每次返回一条记录项,DIR* 指针指向下一条记录项
5. rewinddir
功能:把目录指针恢复到目录的起始位置。
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
6. telldir/seekdir
#include <dirent.h>
long telldir(DIR *dirp);
#include <dirent.h>
void seekdir(DIR *dirp, long offset);
7. closedir
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
8. 递归遍历目录
递归列出目录中的文件列表
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#define MAX_PATH 1024
/* dirwalk: apply fcn to all files in dir */
void dirwalk(char *dir, void (*fcn)(char *))
{
char name[MAX_PATH];
struct dirent *dp;
DIR *dfd;
if ((dfd = opendir(dir)) == NULL)
{
fprintf(stderr, "dirwalk: can't open %s\n", dir);
return;
}
while ((dp = readdir(dfd)) != NULL)
{
if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0)
continue; /* skip self and parent */
if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name))
fprintf(stderr, "dirwalk: name %s %s too long\n",dir, dp->d_name);
else
{
sprintf(name, "%s/%s", dir, dp->d_name);
(*fcn)(name);
}
}
closedir(dfd);
}
/* fsize: print the size and name of file "name" */
void fsize(char *name)
{
struct stat stbuf;
if (stat(name, &stbuf) == -1)
{
fprintf(stderr, "fsize: can't access %s\n", name);
return;
}
if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
dirwalk(name, fsize);
printf("%8ld %s\n", stbuf.st_size, name);
}
int main(int argc, char **argv)
{
if (argc == 1) /* default: current directory */
fsize(".");
else
while (--argc > 0)
fsize(*++argv);
return 0;
}
然而这个程序还是不如 ls -r 健壮,它有可能死循环。
九、虚拟文件系统 VFS
Linux 的文件系统是由虚拟文件系统作为媒介搭建起来的,虚拟文件系统 VFS (Virtual File Systems)是 Linux 内核层实现的一种架构,为用户空间提供统一的文件操作接口。它在内核内部为不同的真实文件系统提供一致的抽象接口。
如下图所示,用户应用程序通过系统调用,与内核中的虚拟文件系统交互,操作实际的文件系统和设备。
在上图中可以看出,Linux 文件系统支持多种类型的文件,对多种类型的文件系统进行了抽象。通过一组相同的系统调用接口,Linux 可以在各种设备上实现多种不同的文件系统。例如,write() 函数可以向多种不同的文件系统上写入数据,调用 write() 函数的应用程序不用管文件的具体存储位置和文件系统的类型,但是当写入数据的时候,函数会正常返回。
VFS 是文件系统的接口框架。这个组件导出一组接口,然后将它们抽象到各个文件系统,各个文件系统的具体实现方式差异很大。有两个针对文件系统对象的缓存(inode 和 demry),它们缓存的对象是最近使用过的文件系统。
每个文件系统实现(如 ext4、vfat 等)导出一组通用接口,供 VFS 使用。缓冲区用于缓存文件系统和相关块设备二者之间的请求。例如,对底层设备驱动程序的读写请求会通过缓冲区缓存来传递。这就允许在其中缓存请求,减少访问物理设备的次数,加快访问速度。以最近使用(LRU)列表的形式管理缓冲区缓存。注意,可以使用 sync 命令将缓冲区缓存中的请求发送到存储媒体(迫使所有未写的数据发送到设备驱动程序,进而发送到存储设备)。
Linux 支持各种各样的文件系统格式,如 ext2、ext3、reiserfs、FAT、NTFS、iso9660 等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以 mount 到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用 ls 命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢?Linux 内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(VFS,Virtual Filesystem)。
1. 文件系统类型
Linux 的文件系统用一组通用对象来表示,这些对象是超级块(superblock)、节点索引(inode)、目录结构(dentry)和文件(file)。
超级块是每种文件系统的根,用于描述和维护文件系统的状态。文件系统中管理的每个对象(文件或目录)在 Linux 中表示为一个索引节点 inode 。
inode 包含管理文件系统中的对象所需的所有元数据(包括可以在对象上执行的操作)。 另一组结构称为 dentry,它们用来实现名称和 inode 之间的映射,有一个目录缓存用来保存最近使用的 dentry。
dentry 还维护目录和文件之间的关系,支持目录和文件在文件系统中的移动。VFS 文件表示一个打开的文件(保存打开的文件的状态,像文件的读偏移量和写偏移量等)。
struct file_system_type
{
const char *name; /* 文件类型名称 */
int fs_flags; /* 标志 */
struct super_block *(*read_super)(struct super_block *, void *,int); /* 读超级块函数 */
struct module *owner; /* 所有者 */
struct file_system_type *next; /* 下一个文件类型 */
struct list_head fs_supers; /* 头结构 */
}
可以使用一组注册函数在 Linux 中动态地添加或删除文件系统。Linux 的内核中保存系统所支持的文件系统的列表,可以通过 /proc 文件系统在用户空间中查看这个列表。虚拟文件系统还显示当前系统中与文件系统相关联的具体设备。在 Linux 中添加新文件系统的方法是调用 register_filesystem。这个函数的参数定义一个文件系统结构(file_system_type)的引用,这个结构定义文件系统的名称、一组属性和两个超级块函数,也可以注销文件系统。
在注册新的文件系统时,会把这个文件系统和它的相关信息添加到 file_systems 列表中。在命令行上输入 cat/proc/filesystems,就可以查看这个列表。例如:
$ cat /proc/filesystems
nodev sysfs /* sys 文件 */
nodev rootfs /* root 文件 */
nodev proc /* proc 文件 */
nodev sockfs /* 套接字文件 */
nodev pipefs /* 管道文件 */
nodev tmpfs /* 临时文件 */
nodev ramfs /* 内存文件 */
nodev mqueue /* 队列 */
nodev usbfs /* USB 文件 */
ext3 /* EXT3类型文件 */
nodev vmhgfs /* 虚拟机 */
nodev vmblock
2. 超级块
超级块结构用来表示一个文件系统,结构如下:
struct super_block
{
...
unsigned long long s_maxbytes; /* 最大文件尺寸 */
struct file_system_type *s_type; /* 文件的类型 */
const struct super_operations *s_op; /* 超级块的操作,主要是对 inode 的操作 */
char s_id[32]; /* 文件系统的名称 */
}
由于篇幅的关系省略了很多信息,读者可以从 Unux/fs.h 文件中获得全部的代码。这个结构包含一个文件系统所需要的信息,例如文件系统名称、文件系统中最大文件的大小, 以及对 inode 块的操作函数等。在 Linux 系统中每种文件类型都有一个超级块,例如,如果系统中存在 ext4 和 vfat ,则存在两个超级块,分别表示 ext4 文件系统和 vfat 文件系统。
struct super一operations
{
struct inode *(*alloc_inode) (struct super_block *sb);/* 申请节点 */
void (*destroy_node) (struct inode *); /* 销毀节点 */
void (*dirty_inode) (struct inode *);
int (*write_inode) (struct inode *, int); /* 写节点 */
void (*drop_inode) (struct inode *); /* 摘取节点 */
};
超级块中的一个重要元素是超级块操作函数的定义。这个结构定义一组用来管理这个文件系统中的节点索引 inode 的函数。例如,可以使用函数 alloc_inode() 来分配 inode,用函数 destroy_inode() 删除 inode。可以用 read_inode() 和 write_inode() 读写 inode,用 sync_fs() 执行文件系统同步。可以在 Linux 的源代码树的文件 Linux/include/Linux/fs.h 中找到 super_operations 结构。Linux 文件系统中所支持的每个文件系统都实现一套自己的 inode 操作方法,这些方法实现超级块所定义的功能并向 VFS 层提供通用的抽象。
3. 文件操作
在文件 fs.h 中定义了文件操作的结构,通常实际的文件系统都要实现对应的操作函数,例如打开文件 open、关闭文件 close、读取数据 read 和写入数据 write 等。
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char _user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char _user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /* 异步读 */
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /* 异步写 */
int (*readdir) (struct file *, void *, filldir_t) ; /* 读目录 */
unsigned int (*poll) (struct file *, struct poll_table_struct *); /* poll 操作 */
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); /* ioctl 函数 */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 非锁定 ioctl */
long (*compat__ioctl) (struct file *, unsigned int, unsigned long); /* 简装 ioctl */
int (*mmap) (struct file *, struct vm_area_struct *); /* 内存映射 */
int (*open) (struct inode *, struct file *) ; /* 打开 */
int (♦flush) (struct file *, fl_owner_t id); /* 写入 */
int (♦release) (struct inode *, struct file *); /* 释放 */
int (*fsync) (struct file *, struct dentry *, int datasync); /* 同步 */
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock +); /* 锁定 */
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file •, int, struct file_lock *);
ssize_t (*splice_write) (struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *,long, struct file_lock **);
ext4 文件系统实现了如下的文件操作,当打开一个 ext2 格式的文件时,系统调用 ext4 文件系统注册的 open() 函数,即函数 generic_file_open()。
const struct file_operations ext4_file_operations =
{
.llseek = generic_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write,
.unlocked_loctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat__ioctl,
#endif
.mmap = generic_file_mmap,
.open = generic_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.splice_read = generic_file_splice_read,
.splice_write = generic_f ile_splice_write,
};
4. dup/dup2
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup 和 dup2 都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个 file 结构体。如果两个文件描述符指向同一个 file 结构体,File Status Flag 和读写位置只保存一份在 file 结构体中,并且 file 结构体的引用计数是 2。如果两次 open 同一文件得到两个文件描述符,则每个描述符对应一个不同的 file 结构体,可以有不同的 File Status Flag 和读写位置。请注意区分这两种情况。
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int fd, save_fd;
char msg[] = "This is a test\n";
fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
if(fd<0)
{
perror("open");
exit(1);
}
save_fd = dup(STDOUT_FILENO);
dup2(fd, STDOUT_FILENO);
close(fd);
write(STDOUT_FILENO, msg, strlen(msg));
dup2(save_fd, STDOUT_FILENO);
write(STDOUT_FILENO, msg, strlen(msg));
close(save_fd);
return 0;
}