文件系统操作
- 前言
- 文件系统在 Linux 操作系统中的位置
- 硬件管理和设备驱动
- 理解、查看磁盘分区
- 分区和文件系统的关系
- 分区、逻辑卷和文件系统的关系
- 文件系统的可视化结构
- 如何制作一个文件系统
- 如何开发自己的文件系统
- 后记
文件系统在 Linux 操作系统中的位置
如何来认识文件系统呢?从 Shell 程序员的角度来看,文件系统就是一个用来组织各种文件的方法。但是文件系统无法独立于硬件存储设备和操作系统而存在,因此还是有必要来弄清楚硬件存储设备、分区、操作系统、逻辑卷、文件系统等各种概念之间的联系,以便理解文件系统常规操作的一些“细节”。这个联系或许(也许会有一些问题)可以通过这样一种方式来呈现:
从图中可以清晰地看到各个“概念”之间的关系,它们以不同层次分布,覆盖硬件设备、系统内核空间、系统用户空间。在用户空间,用户可以不管内核如何操作具体硬件设备,仅仅使用程序员设计的各种界面就可以,而普通程序员也仅仅需要利用内核提供的各种接口(System Call)或者一些C库来和内核进行交互,而无须关心具体的实现细节。不过对于操作系统开发人员,他们需要在内核空间设计特定的数据结构来管理和组织底层的硬件设备。
下面从下到上的方式(即从底层硬件开始),用工具来分析和理解图中几个重要概念。(如果有兴趣,可以先看看下面的几则资料)
参考资料:
硬件管理和设备驱动
Linux 系统通过设备驱动管理硬件设备。如果添加了新的硬件设备,那么需要编写相应的硬件驱动来管理它。对于一些常见的硬件设备,系统已经自带了相应的驱动,编译内核时,选中它们,然后编译成内核的一部分或者以模块的方式编译。如果以模块的方式编译,那么可以在系统的 /lib/modules/$(uname -r)
目录下找到对应的模块文件。
范例:查找设备所需的驱动文件
比如,可以这样找到相应的 scsi 驱动和 usb 驱动对应的模块文件:
更新系统中文件索引数据库(有点慢)
$ updatedb
查找 scsi 相关的驱动
$ locate scsi*.ko
查找 usb 相关的驱动
$ locate usb*.ko
这些驱动以 .ko
为后缀,在安装系统时默认编译为了模块。实际上可以把它们编译为内核的一部分,仅仅需要在编译内核时选择为[*]
即可。但是,很多情况下会以模块的方式编译它们,这样可以减少内核的大小,并根据需要灵活地加载和卸载它们。下面简单地演示如何卸载模块、加载模块以及查看已加载模块的状态。
可通过 /proc
文件系统的 modules
文件检查内核中已加载的各个模块的状态,也可以通过 lsmod
命令直接查看它们。
$ cat /proc/modules
或者
$ lsmod
范例:查看已经加载的设备驱动
查看 scsi 和 usb 相关驱动,结果各列为模块名、模块大小、被其他模块的引用情况(引用次数、引用它们的模块)
$ lsmod | egrep "scsi|usb"
usbhid 29536 0
hid 28928 1 usbhid
usbcore 138632 4 usbhid,ehci_hcd,ohci_hcd
scsi_mod 147084 4 sg,sr_mod,sd_mod,libata
范例:卸载设备驱动
下面卸载 usbhid
模块看看(不要卸载scsi的驱动!因为你的系统可能就跑在上面,如果确实想玩玩,卸载前记得保存数据),通过 rmmod
命令就可以实现,先切换到 Root 用户:
$ sudo -s
# rmmod usbhid
再查看该模块的信息,已经看不到了吧
$ lsmod | grep ^usbhid
范例:挂载设备驱动
如果有个 usb 鼠标,那么移动一下,是不是发现动不了啦?因为设备驱动都没有了,设备自然就没法用罗。不过不要紧张,既然知道原因,那么重新加载驱动就可以,下面用 insmod
把 usbhid
模块重新加载上。
$ sudo -s
# insmod `locate usbhid.ko`
locate usbhid.ko
是为了找出 usbhid.ko
模块的路径,如果之前没有 updatedb
,估计用它是找不到了,不过也可以直接到 /lib/modules
目录下用 find
把 usbhid.ko
文件找到。
# insmod $(find /lib/modules -name "*usbhid.ko*" | grep `uname -r`)
现在鼠标又可以用啦,不信再动一下鼠标 :-)
到这里,硬件设备和设备驱动之间关系应该是比较清楚了。如果没有,那么继续下面的内容。
范例:查看设备驱动对应的设备文件
Linux 设备驱动关联着相应的设备文件,而设备文件则和硬件设备一一对应。这些设备文件都统一存放在系统的 /dev/
目录下。
例如,scsi 设备对应/dev/sda
,/dev/sda1
,/dev/sda2
... 下面查看这些设备信息。
$ ls -l /dev/sda*
brw-rw---- 1 root disk 8, 0 2007-12-28 22:49 /dev/sda
brw-rw---- 1 root disk 8, 1 2007-12-28 22:50 /dev/sda1
brw-rw---- 1 root disk 8, 3 2007-12-28 22:49 /dev/sda3
brw-rw---- 1 root disk 8, 4 2007-12-28 22:49 /dev/sda4
brw-rw---- 1 root disk 8, 5 2007-12-28 22:50 /dev/sda5
brw-rw---- 1 root disk 8, 6 2007-12-28 22:50 /dev/sda6
brw-rw---- 1 root disk 8, 7 2007-12-28 22:50 /dev/sda7
brw-rw---- 1 root disk 8, 8 2007-12-28 22:50 /dev/sda8
可以看到第一列第一个字符都是 b
,第五列都是数字 8 。 b
表示该文件是一个块设备文件,对应地,如果是 c
则表示字符设备(例如 `/dev/ttyS0),关于块设备和字符设备的区别,可以看这里:
- 字符设备:字符设备就是能够像字节流一样访问的设备,字符终端和串口就属于字符设备。
- 块设备:块设备上可以容纳文件系统。与字符设备不同,在读写时,块设备每次只能传输一个或多个完整的块。在 Linux 操作系统中,应用程序可以像访问字符设备一样读写块设备(一次读取或写入任意的字节数据)。因此,块设备和字符设备的区别仅仅是在内核中对于数据的管理不同。
数字 8 则是该硬件设备在内核中对应的设备编号,可以在内核的 Documentation/devices.txt
和 /proc/devices
文件中找到设备号分配情况。但是为什么同一个设备会对应不同的设备文件(/dev/sda
后面为什么还有不同的数字,而且 ls
结果中的第 6 列和它们对应起来)。这实际上是为了区分不同设备的不同部分。对于硬盘,这样可以处理硬盘内部的不同分区。就内核而言,它仅仅需要通过第 5 列的设备号就可以找到对应的硬件设备,但是对于驱动模块来说,它还需要知道如何处理不同的分区,于是就多了一个辅设备号,即第 6 列对应的内容。这样一个设备就有了主设备号(第 5 列)和辅设备号(第 6 列),从而方便地实现对各种硬件设备的管理。
因为设备文件和硬件是对应的,这样可以直接从 /dev/sda
(如果是 IDE
的硬盘,那么对应的设备就是 /dev/hda
啦)设备中读出硬盘的信息,例如:
范例:访问设备文件
用 dd
命令复制出硬盘的前 512 个字节,要 Root 用户
$ sudo dd if=/dev/sda of=mbr.bin bs=512 count=1
用 file
命令查看相应的信息
$ file mbr.bin
mbr.bin: x86 boot sector, LInux i386 boot LOader; partition 3: ID=0x82, starthead 254, startsector 19535040, 1959930 sectors; partition 4: ID=0x5, starthead 254, startsector 21494970, 56661255 sectors, code offset 0x48
也可以用 od
命令以 16 进制的形式读取并进行分析
$ od -x mbr.bin
bs
是块的大小(以字节 bytes
为单位),count
是块数
因为这些信息并不直观(而且下面会进一步深入分析),那么先来看看另外一个设备文件,将可以非常直观地演示设备文件和硬件的对应关系。还是以鼠标为例吧,下面来读取鼠标对应的设备文件的信息。
$ sudo -s
# cat /dev/input/mouse1 | od -x
你的鼠标驱动可能不太一样,所以设备文件可能是其他的,但是都会在 /dev/input
下。
移动鼠标看看,是不是发现有不同信息输出。基于这一原理,我们经常通过在一端读取设备文件 /dev/ttyS0
中的内容,而在另一端往设备文件 /dev/ttyS0
中写入内容来检查串口线是否被损坏。
到这里,对设备驱动、设备文件和硬件设备之间的关联应该是印象更深刻了。如果想深入了解设备驱动的工作原理和设备驱动的编写,那么看看下面列出的相关资料,开始设备驱动的编写历程吧。
参考资料:
- Compile linux kernel 2.6
- Linux 系统的硬件驱动程序编写原理
- Linux 下 USB设备的原理、配置、常见问题
- The Linux Kernel Module Programming Guide
- Linux 设备驱动开发
理解、查看磁盘分区
实际上内存、u 盘等都可以作为文件系统底层的“存储”设备,但是这里仅用硬盘作为实例来介绍磁盘和分区的关系。
目前 Linux 的分区依然采用第一台PC硬盘所使用的分区原理,下面逐步分析和演示这一分区原理。
磁盘分区基本原理
先来看看几个概念:
设备管理和分区
Linux 下,每一个存储设备对应一个系统的设备文件,对于硬盘等
IDE
和SCSI
设备,在系统的/dev
目录下可以找到对应的包含字符hd
和sd
的设备文件。而根据硬盘连接的主板设备接口和数据线接口的不同,在hd
或者sd
字符后面可以添加一个从a
到z
的字符,例如hda
,hdb
,hdc
和sda
,sdb
,sdc
等,另外为了区别同一个硬件设备的不同分区,在后面还可以添加了一个数字,例如hda1
,hda2
,hda3
和sda1
,sda2
,sda3
,所以在/dev
目录下,可以看到很多类似的设备文件。各分区的作用
在分区时常遇到主分区和逻辑分区的问题,这实际上是为了方便扩展分区,正如后面的逻辑卷的引入是为了更好地管理多个硬盘一样,引入主分区和逻辑分区可以方便地进行分区的管理。
Linux 系统中每一个硬盘设备最多由 4 个主分区(包括扩展分区)构成。
主分区的作用是计算机用来进行启动操作系统的,因此每一个操作系统的启动程序或者称作是引导程序,都应该存放在主分区上。 Linux 规定主分区(或者扩展分区)占用分区编号中的前 4 个。所以会看到主分区对应的设备文件为 /dev/hda1-4
或者 /dev/sda1-4
,而不会是 hda5
或者 sda5
。
扩展分区则是为了扩展更多的逻辑分区的,在 Linux 下,逻辑分区占用了 hda5-16
或者 sda5-16
等 12 个编号。
- 分区类型
它规定了这个分区上的文件系统的类型。Linux支持诸如msdoc,vfat,ext2,ext3等诸多的文件系统类型,更多信息在下一小节进行进一步的介绍。
分区和文件系统的关系
在没有引入逻辑卷之前,分区类型和文件系统类型几乎可以同等对待,设置分区类型的过程就是格式化分区,建立相应的文件系统类型的过程。
下面主要介绍如何建立分区和文件系统类型的联系,即如何格式化分区为指定的文件系统类型。
常见分区类型
先来看看 Linux 下文件系统的常见类型(如果要查看所有 Linux 支持的文件类型,可以用 fdisk
命令的 l
命令查看,或者通过 man fs
查看,也可通过 /proc/filesystems
查看到当前内核支持的文件系统类型)
ext2
,ext3
,ext4
:这三个是 Linux 根文件系统通常采用的类型swap
:这个是实现 Linux 虚拟内存时采用的一种文件系统,安装时一般需要建立一个专门的分区,并格式化为swap
文件系统(如果想添加更多swap
分区,可以参考本节的参考资料,熟悉dd
,mkswap
,swapon
,swapoff
等命令的用法)proc
:这是一种比较特别的文件系统,作为内核和用户之间的一个接口存在,建立在内存中(可以通过cat
命令查看/proc
系统下的文件,甚至可以通过修改/proc/sys
下的文件实时调整内核配置,当前前提是需要把proc
文件系统挂载上:mount -t proc proc /proc
除了上述文件系统类型外,Linux 支持包括 vfat
,iso
,xfs
,nfs
在内各种常见的文件系统类型,在 Linux 下,可以自由地查看和操作 Windows 等其他操作系统使用的文件系统。
那么如何建立磁盘和这些文件系统类型的关联呢?格式化。
格式化的过程实际上就是重新组织分区的过程,可通过 mkfs
命令来实现,当然也可以通过 fdisk
等命令来实现。这里仅介绍 mkfs
,mkfs
可用来对一个已有的分区进行格式化,不能实现分区操作(如果要对一个磁盘进行分区和格式化,那么可以用 fdisk
)。格式化后,相应分区上的数据就会通过某种特别的文件系统类型进行组织。
范例:格式化文件系统
例如:把 /dev/sda9
分区格式化为 ext3
的文件系统。
$ sudo -s
# mkfs -t ext3 /dev/sda9
如果要列出各个分区的文件系统类型,那么可以用 fdisk -l
命令。
更多信息请参考下列资料。
参考资料:
- Linux 下加载 swap 分区的步骤
- Linux 下 ISO 镜像文件的制作与刻录
- RAM 磁盘分区解释: [1], [2]
- 高级文件系统实现者指南
分区、逻辑卷和文件系统的关系
上一节直接把分区格式化为某种文件系统类型,但是考虑到扩展新的存储设备的需要,开发人员在文件系统和分区之间引入了逻辑卷。考虑到时间关系,这里不再详述,请参考资料:Linux 逻辑卷管理详解
文件系统的可视化结构
文件系统最终呈现出来的是一种可视化的结构,可用ls,find,tree等命令把它呈现出来。它就像一颗倒挂的“树”,在树的节点上还可以挂载新的“树”。
下面简单介绍文件系统的挂载。
一个文件系统可以通过一个设备挂载(mount
)到某个目录下,这个目录被称为挂载点。有趣的是,在 Linux 下,一个目录本身还可以挂载到另外一个目录下,一个格式化了的文件也可以通过一个特殊的设备 /dev/loop
进行挂载(如 iso
文件)。另外,就文件系统而言,Linux 不仅支持本地文件系统,还支持远程文件系统(如 nfs
)。
范例:挂载文件系统
下面简单介绍文件系统挂载的几个实例。
- 根文件系统的挂载
挂载需要 Root 权限,例如,挂载系统根文件系统 /dev/sda1
到 /mnt
$ sudo -s
# mount -t ext3 /dev/sda1 /mnt/
查看 /dev/sda1
的挂载情况,可以看到,一个设备可以多次挂载
$ mount | grep sda1
/dev/sda1 on / type ext3 (rw,errors=remount-ro)
/dev/sda1 on /mnt type ext3 (rw)
对于一个已经挂载的文件系统,为支持不同属性可以重新挂载
$ mount -n -o remount, rw /
- 挂载一个新增设备
如果内核已经支持 USB 接口,那么插入 u 盘时,可以通过 dmesg
命令查看对应的设备号,并挂载它。
查看 dmesg
结果中的最后几行内容,找到类似 /dev/sdN
的信息,找出 u 盘对应的设备号
$ dmesg
这里假设 u 盘是 vfat
格式,以便在一些打印店里的 Windows 上也可使用
# mount -t vfat /dev/sdN /path/to/mountpoint_directory
- 挂载一个 iso 文件或者是光盘
对于一些iso文件或者是 iso 格式的光盘,同样可以通过 mount
命令挂载。
对于 iso 文件:
# mount -t iso9660 /path/to/isofile /path/to/mountpoint_directory
对于光盘:
# mount -t iso9660 /dev/cdrom /path/to/mountpoint_directory
- 挂载一个远程文件系统
# mount -t nfs remote_ip:/path/to/share_directory /path/to/local_directory
- 挂载一个 proc 文件系统
# mount -t proc proc /proc
proc
文件系统组织在内存中,但是可以把它挂载到某个目录下。通常把它挂载在 /proc
目录下,以便一些系统管理和配置工具使用它。例如 top
命令用它分析内存的使用情况(读取 /proc/meminfo
和 /proc/stat
等文件中的内容); lsmod
命令通过它获取内核模块的状态(读取 /proc/modules
); netstat
命令通过它获取网络的状态(读取 /proc/net/dev
等文件)。当然,也可以编写相关工具。除此之外,通过调整 /proc/sys
目录下的文件,可以动态地调整系统配置,比如往 /proc/sys/net/ipv4/ip_forward
文件中写入数字 1 就可以让内核支持数据包转发。(更多信息请参考 proc
的帮助,man
proc
)
- 挂载一个目录
$ mount --bind /path/to/needtomount_directory /path/to/mountpoint_directory
这个非常有意思,比如可以把某个目录挂载到 ftp 服务的根目录下,而无须把内容复制过去,就可以把相应目录中的资源提供给别人共享。
范例:卸载某个分区
以上都只提到了挂载,那怎么卸载呢?用 umount
命令跟上挂载的源地址或者挂载点(设备,文件,远程目录等)就可以。例如:
$ umount /path/to/mountpoint_directory
或者
$ umount /path/to/mount_source
如果想管理大量的或者经常性的挂载服务,那么每次手动挂载是很糟糕的事情。这时就可利用 mount
的配置文件 /etc/fstab
,把 mount
对应的参数写到 /etc/fstab
文件对应的列中即可实现批量挂载( mount -a
)和卸载( umount -a
)。 /etc/fstab
中各列分别为文件系统、挂载点、类型、相关选项。更多信息可参考 fstab
的帮助( man fstab
)。
参考资料:
如何制作一个文件系统
Linux 文件系统下有一些最基本的目录,不同的目录下存放着不同作用的各类文件。最基本的目录有 /etc
,/lib
,/dev
,/bin
等,它们分别存放着系统配置文件,库文件,设备文件和可执行程序。这些目录一般情况下是必须的,在做嵌入式开发时,需要手动或者是用 busybox
等工具来创建这样一个基本的文件系统。这里仅制作一个非常简单的文件系统,并对该文件系统进行各种常规操作,以便加深对文件系统的理解。
范例:用 dd 创建一个固定大小的文件
还记得 dd
命令么?就用它来产生一个固定大小的文件,这个为 1M(1024\*1024 bytes)
的文件
$ dd if=/dev/zero of=minifs bs=1024 count=1024
查看文件类型,这里的 minifs
是一个充满 \\0
的文件,没有任何特定的数据结构
$ file minifs
minifs: data
说明: /dev/zero
是一个非常特殊的设备,如果读取它,可以获取任意多个 \\0
。
接着把该文件格式化为某个指定文件类型的文件系统。(是不是觉得不可思议,文件也可以格式化?是的,不光是设备可以,文件也可以以某种文件系统类型进行组织,但是需要注意的是,某些文件系统(如 ext3
)要求被格式化的目标最少有 64M
的空间)。
范例:用 mkfs 格式化文件
$ mkfs.ext2 minifs
查看此时的文件类型,这时文件 minifs
就以 ext2
文件系统的格式组织了
$ file minifs
minifs: Linux rev 1.0 ext2 filesystem data
范例:挂载刚创建的文件系统
因为该文件以文件系统的类型组织了,那么可以用 mount
命令挂载并使用它。
请切换到 root
用户挂载它,并通过 -o loop
选项把它关联到一个特殊设备 /dev/loop
$ sudo -s
# mount minifs /mnt/ -o loop
查看该文件系统信息,仅可以看到一个目录文件 lost+found
$ ls /mnt/
lost+found
范例:对文件系统进行读、写、删除等操作
在该文件系统下进行各种常规操作,包括读、写、删除等。(每次操作前先把 minifs
文件保存一份,以便比较,结合相关资料就可以深入地分析各种操作对文件系统的改变情况,从而深入理解文件系统作为一种组织数据的方式的实现原理等)
$ cp minifs minifs.bak
$ cd /mnt
$ touch hello
$ cd -
$ cp minifs minifs-touch.bak
$ od -x minifs.bak > orig.od
$ od -x minifs-touch.bak > touch.od
创建一个文件后,比较此时文件系统和之前文件系统的异同
$ diff orig.od touch.od
diff orig.od touch.od
61,63c61,64
< 0060020 000c 0202 2e2e 0000 000b 0000 03e8 020a
< 0060040 6f6c 7473 662b 756f 646e 0000 0000 0000
< 0060060 0000 0000 0000 0000 0000 0000 0000 0000
---
> 0060020 000c 0202 2e2e 0000 000b 0000 0014 020a
> 0060040 6f6c 7473 662b 756f 646e 0000 000c 0000
> 0060060 03d4 0105 6568 6c6c 006f 0000 0000 0000
> 0060100 0000 0000 0000 0000 0000 0000 0000 0000
通过比较发现:添加文件,文件系统的相应位置发生了明显的变化
$ echo "hello, world" > /mnt/hello
执行 sync
命令,确保缓存中的数据已经写入磁盘(还记得本节图 1 的 buffer cache
吧,这里就是把 cache
中的数据写到磁盘中)
$ sync
$ cp minifs minifs-echo.bak
$ od -x minifs-echo.bak > echo.od
写入文件内容后,比较文件系统和之前的异同
$ diff touch.od echo.od
查看文件系统中的字符串
$ strings minifs
lost+found
hello
hello, world
删除 hello
文件,查看文件系统变化
$ rm /mnt/hello
$ cp minifs minifs-rm.bak
$ od -x minifs-rm.bak > rm.od
$ diff echo.od rm.od
通过查看文件系统的字符串发现:删除文件时并没有覆盖文件内容,所以从理论上说内容此时还是可恢复的
$ strings minifs
lost+found
hello
hello, world
上面仅仅演示了一些分析文件系统的常用工具,并分析了几个常规的操作,如果想非常深入地理解文件系统的实现原理,请熟悉使用上述工具并阅读相关资料。
参考资料:
- Build a mini filesystem in linux from scratch
- Build a mini filesystem in linux with BusyBox
- ext2 文件系统
文件操作
前言
下面先来介绍文件的各种属性,然后介绍普通文件的一般操作。
文件的各种属性
首先通过文件的结构体来看看文件到底有哪些属性:
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
表示管道文件,b
和 c
分别表示块设备和字符设备(另外 s
表示 socket
文件)。在 stat
命令的结果中,可以在第二行的最后找到说明,从上面的操作可以看出,directory_file
是目录,stat
命令的结果中用 directory
表示,而 null_char_dev_file
它则用 character special file
说明。
范例:简单比较它们的异同
对于普通文件:就是一系列字符的集合,所以可以读、写等
$ echo "hello, world" > regular_file
$ cat regular_file
hello, world
在目录中可以创建新文件,所以目录还有叫法:文件夹,到后面会分析目录文件的结构体,它实际上存放了它下面的各个文件的文件名。
$ cd directory_file
$ touch file1 file2 file3
对于有名管道,操作起来比较有意思:如果要读它,除非有内容,否则阻塞;如果要写它,除非有人来读,否则阻塞。它常用于进程通信中。可以打开两个终端 terminal1
和 terminal2
,试试看:
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
命令查看它们之间的区别,包括它们的 Blocks
,inode
等值,也可以考虑用 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 作为一个多用户系统,为多用户使用同一个系统提供了极大的方便,比如对于系统上的文件,它通过属主来区分不同的用户,以便分配它们对不同文件的操作权限。为了更方便地管理,文件属主包括该文件所属用户,以及该文件所属的用户组,因为用户可以属于多个组。先来简单介绍 Linux 下用户和组的管理。
Linux 下提供了一组命令用于管理用户和组,比如用于创建用户的 useradd
和 groupadd
,用于删除用户的 userdel
和 groupdel
,另外,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 -l
命令的结果的第一列的后 9 个字符中,可以看到类似这样的信息 rwxr-xr-x
,它们对应于文件结构体的 st_mode
部分(st_mode
包含文件类型信息和文件权限信息两部分)。这类信息可以分成三部分,即 rwx
,r-x
,r-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 #普通用户通过执行该命令,修改自己的密码
说明:
setuid
和setgid
位是让普通用户可以以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》第十章)。因此,对于内核而言,通过主设备号就可以找到对应的驱动去识别某个设备,而对于驱动而言,为了能够更复杂地访问设备,比如访问设备的不同部分(如硬件通过分区分成不同部分,而出现 hda1
,hda2
,hda3
等),比如产生不同要求的随机数(如 /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
去阅读,因为即使没有做任何修改操作,一旦执行了保存命令,将修改文件的时间戳信息。
文件名
文件名并没有存放在文件结构体内,而是存放在它所在的目录结构体中。所以,在目录的同一级别中,文件名必须是唯一的。
文件的基本操作
范例:创建文件
socket
文件是一类特殊的文件,可以通过 C 语言创建,这里不做介绍(暂时不知道是否可以用命令直接创建),其他文件将通过命令创建。
$ touch regular_file #创建普通文件
$ mkdir directory_file #创建目录文件,目录文件里头可以包含更多文件
$ ln regular_file regular_file_hard_link #硬链接,是原文件的一个完整拷比
$ ln -s regular_file regular_file_soft_link #类似一个文件指针,指向原文件
$ mkfifo fifo_pipe #或者通过 "mknod fifo_pipe p" 来创建,FIFO满足先进先出的特点
$ mknod hda1_block_dev_file b 3 1 #块设备
$ mknod null_char_dev_file c 1 3 #字符设备
创建一个文件实际上是在文件系统中添加了一个节点(inode),该节点信息将保存到文件系统的节点表中。更形象地说,就是在一颗树上长了一颗新的叶子(文件)或者枝条(目录文件,上面还可以长叶子的那种),这些可以通过
tree命令或者
ls` 命令形象地呈现出来。文件系统从日常使用的角度,完全可以当成一颗倒立的树来看,因为它们太像了,太容易记忆啦。
$ tree 当前目录
或者
$ ls 当前目录
范例:删除文件
删除文件最直接的印象是这个文件再也不存在了,这同样可以通过 ls
或者 tree
命令呈现出来,就像树木被砍掉一个分支或者摘掉一片叶子一样。实际上,这些文件删除之后,并不是立即消失了,而是仅仅做了删除标记,因此,如果删除之后,没有相关的磁盘写操作把相应的磁盘空间“覆盖”,那么原理上是可以恢复的(虽然如此,但是这样的工作往往很麻烦,所以在删除一些重要数据时,请务必三思而后行,比如做好备份工作),相应的做法可以参考后续资料。
具体删除文件的命令有 rm
,如果要删除空目录,可以用 rmdir
命令。例如:
$ rm regular_file
$ rmdir directory_file
$ rm -r directory_file_not_empty
rm
有两个非常重要的参数,一个是 -f
,这个命令是非常“野蛮的”,它估计给很多 Linux user 带来了痛苦,另外一个是 -i
,这个命令是非常“温柔的”,它估计让很多用户感觉烦躁不已。用哪个还是根据您的“心情”吧,如果做好了充分的备份工作,或者采取了一些有效避免灾难性后果的动作的话,您在做这些工作的时候就可以放心一些啦。
范例:复制文件
文件的复制通常是指文件内容的“临时”复制。通过这一节开头的介绍,我们应该了解到,文件的硬链接和软链接在某种意义上说也是“文件的复制”,前者同步复制文件内容,后者在读写的情况下同步“复制”文件内容。例如:
用 cp
命令常规地复制文件(复制目录需要 -r
选项)
$ cp regular_file regular_file_copy
$ cp -r diretory_file directory_file_copy
创建硬链接(link
和 copy
不同之处是后者是同步更新,前者则不然,复制之后两者不再相关)
$ ln regular_file regular_file_hard_link
创建软链接
$ ln -s regular_file regluar_file_soft_link
范例:修改文件名
修改文件名实际上仅仅修改了文件名标识符。可以通过 mv
命令来实现修改文件名操作(即重命名)。
$ mv regular_file regular_file_new_name
范例:编辑文件
下面主要简单介绍 Linux 下通过重定向来实现文件的这些常规的编辑操作。
创建一个文件并写入 abcde
$ echo "abcde" > new_regular_file
再往上面的文件中追加一行 abcde
$ echo "abcde" >> new_regular_file
按行读一个文件
$ while read LINE; do echo $LINE; done < test.sh
提示:如果要把包含重定向的字符串变量当作命令来执行,请使用 eval
命令,否则无法解释重定向。例如,
$ redirect="echo \"abcde\" >test_redirect_file"
$ $redirect #这里会把>当作字符 > 打印出来,而不会当作 重定向 解释
"abcde" >test_redirect_file
$ eval $redirect #这样才会把 > 解释成 重定向
$ cat test_redirect_file
abcde
范例:压缩/解压缩文件
压缩和解压缩文件在一定意义上来说是为了方便文件内容的传输,不过也可能有一些特定的用途,比如内核和文件系统的映像文件等(更多相关的知识请参考后续资料)。
这里仅介绍几种常见的压缩和解压缩方法:
tar
$ tar -cf file.tar file #压缩
$ tar -xf file.tar #解压
gz
$ gzip -9 file
$ gunzip file
tar.gz
$ tar -zcf file.tar.gz file
$ tar -zxf file.tar.gz
bz2
$ bzip2 file
$ bunzip2 file
tar.bz2
$ tar -jcf file.tar.bz2 file
$ tar -jxf file.tar.bz2
通过上面的演示,应该已经非常清楚 tar
,bzip2,
bunzip2,gzip,
gunzip命令的角色了吧?如果还不清楚,多操作和比较一些上面的命令,并查看它们的手册:
man tar`...
范例:文件搜索(文件定位)
文件搜索是指在某个目录层次中找出具有某些属性的文件在文件系统中的位置,这个位置如果扩展到整个网络,那么可以表示为一个 URL
地址,对于本地的地址,可以表示为 file://+
本地路径。本地路径在 Linux 系统下是以 /
开头,例如,每个用户的家目录可以表示为: file:///home/
。下面仅仅介绍本地文件搜索的一些办法。
find
命令提供了一种“及时的”搜索办法,它根据用户的请求,在指定的目录层次中遍历所有文件直到找到需要的文件为止。而 updatedb+locate
提供了一种“快速的”的搜索策略,updatedb
更新并产生一个本地文件数据库,而 locate
通过文件名检索这个数据库以便快速找到相应的文件。前者支持通过各种文件属性进行搜索,并且提供了一个接口(-exec
选项)用于处理搜索后的文件。因此为“单条命令”脚本的爱好者提供了极大的方便,不过对于根据文件名的搜索而言,updatedb+locate
的方式在搜索效率上会有明显提高。下面简单介绍这两种方法:
find
命令基本使用演示
$ find ./ -name "*.c" -o -name "*.h" #找出所有的C语言文件,-o是或者
$ find ./ \( -name "*.c" -o -name "*.h" \) -exec mv '{}' ./c_files/ \;
# 把找到的文件移到c_files下,这种用法非常有趣
上面的用法可以用 xargs
命令替代
$ find ./ -name "*.c" -o -name "*.h" | xargs -i mv '{}' ./c_files/
# 如果要对文件做更复杂的操作,可以考虑把mv改写为你自己的处理命令,例如,我需要修
改所有的文件名后缀为大写。
$ find ./ -name "*.c" -o -name "*.h" | xargs -i ./toupper.sh '{}' ./c_files/
toupper.sh
就是我们需要实现的转换小写为大写的一个处理文件,具体实现如下:
$ cat toupper.sh
#!/bin/bash
# the {} will be expended to the current line and becomen the first argument of this script
FROM=$1
BASENAME=${FROM##*/}
BASE=${BASENAME%.*}
SUFFIX=${BASENAME##*.}
TOSUFFIX="$(echo $SUFFIX | tr '[a-z]' '[A-Z]')"
TO=$2/$BASE.$TOSUFFIX
COM="mv $FROM $TO"
echo $COM
eval $COM
updatedb+locate
基本使用演示
$ updatedb #更新库
$ locate find*.gz #查找包含find字符串的所有gz压缩包
实际上,除了上面两种命令外,Linux 下还有命令查找工具:which
和 whereis
,前者用于返回某个命令的全路径,而后者用于返回某个命令、源文件、man 文件的路径。例如,查找
find` 命令的绝对路径:
$ which find
/usr/bin/find
$ whereis find
find: /usr/bin/find /usr/X11R6/bin/find /usr/bin/X11/find /usr/X11/bin/find /usr/man/man1/find.1.gz /usr/share/man/man1/find.1.gz /usr/X11/man/man1/find.1.gz
需要提到的是,如果想根据文件的内容搜索文件,那么 find
和 updatedb+locate
以及 which
,whereis
都无能为力啦,可选的方法是 grep
,sed
等命令,前者在加上 -r
参数以后可以在指定目录下文件中搜索指定的文件内容,后者再使用 -i
参数后,可以对文件内容进行替换。它们的基本用法在前面的章节中已经详细介绍了,这里就不再赘述。
值得强调的是,这些命令对文件的操作非常有意义。它们在某个程度上把文件系统结构给抽象了,使得对整个文件系统的操作简化为对单个文件的操作,而单个文件如果仅仅考虑文本部分,那么最终却转化成了之前的字符串操作,即上一节讨论过的内容。为了更清楚地了解文件的组织结构,文件之间的关系,在下一节将深入探讨文件系统。