文件系统操作
文件系统在 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等诸多的文件系统类型,更多信息在下一小节进行进一步的介绍。
通过分析 MBR 来理解分区原理
下面通过分析硬盘的前 512 个字节(即 MBR
)来分析和理解分区。
先来看看这张图:
它用来描述 MBR
的结构。 MBR
包括引导部分、分区表、以及结束标记 `(55AAH),分别占用了 512 字节中 446 字节、 64 字节和 2 字节。这里仅仅关注分区表部分,即中间的 64 字节以及图中左边的部分。
由于我用的是 SCSI
的硬盘,下面从 /dev/sda
设备中把硬盘的前 512 个字节拷贝到文件 mbr.bin
中。
$ sudo -s
# dd if=/dev/sda of=mbr.bin bs=512 count=1
下面用 file
,od
,fdisk
等命令来分析这段 MBR
的数据,并对照上图以便加深理解。
$ 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 -x mbr.bin | tail -6 #仅关注中间的64字节,所以截取了结果中后6行
0000660 0000 0000 0000 0000 a666 a666 0000 0180
0000700 0001 fe83 ffff 003f 0000 1481 012a 0000
0000720 0000 0000 0000 0000 0000 0000 0000 fe00
0000740 ffff fe82 ffff 14c0 012a e7fa 001d fe00
0000760 ffff fe05 ffff fcba 0147 9507 0360 aa55
$ sudo -s
# fdisk -l | grep ^/ #仅分析MBR相关的部分,不分析逻辑分区部分
/dev/sda1 * 1 1216 9767488+ 83 Linux
/dev/sda3 1217 1338 979965 82 Linux swap / Solaris
/dev/sda4 1339 4865 28330627+ 5 Extended
file` 命令的结果显示,刚拷贝的 512 字节是启动扇区,用分号分开的几个部分分别是 `bootloader`,分区 3 和分区 4 。分区 3 的类型是 82,即 `swap` 分区(可以通过 `fdisk` 命令的 `l` 命令列出相关信息),它对应 `fdisk` 的结果中 `/dev/sda3` 所在行的第 5 列,分区 3 的扇区数是 1959930,转换成字节数是 `1959930\*512` (目前,硬盘的默认扇区大小是 512 字节),而 `swap` 分区的默认块大小是 1024 字节,这样块数就是 `:
$ echo 1959930*512/1024 | bc
979965
正好是 fdisk
结果中 /dev/sda3
所在行的第四列对应的块数,同样地,可以对照 fdisk
和 file
的结果分析分区 4 。
再来看看 od
命令以十六进制显示的结果,同样考虑分区 3,计算一下发现,分区 3 对应的 od
命令的结果为:
fe00 ffff fe82 ffff 14c0 012a e7fa 001d
首先是分区标记,00H
,从上图中,看出它就不是引导分区(80H
标记的才是引导分区),而分区类型呢?为 82H
,和 file
显示结果一致,现在再来关注一下分区大小,即 file
结果中的扇区数。
$ echo "ibase=10;obase=16;1959930" | bc
1DE7FA
刚好对应 e7fa 001d
,同样地考虑引导分区的结果:
0180 0001 fe83 ffff 003f 0000 1481 012a
分区标记: 80H
,正好反应了这个分区是引导分区,随后是引导分区所在的磁盘扇区情况,010100,即 1 面 0 道 1 扇区。其他内容可以对照分析。
考虑到时间关系,更多细节请参考下面的资料或者查看看系统的相关手册。
补充:安装系统时,可以用 fdisk
,cfdisk
等命令进行分区。如果要想从某个分区启动,那么需要打上 80H
标记,例如可通过 cfdisk
把某个分区设置为 bootable
来实现。
参考资料:
分区和文件系统的关系
在没有引入逻辑卷之前,分区类型和文件系统类型几乎可以同等对待,设置分区类型的过程就是格式化分区,建立相应的文件系统类型的过程。
下面主要介绍如何建立分区和文件系统类型的联系,即如何格式化分区为指定的文件系统类型。
常见分区类型
先来看看 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 逻辑卷管理详解
文件系统的可视化结构
文件系统最终呈现出来的是一种可视化的结构,可用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
上面仅仅演示了一些分析文件系统的常用工具,并分析了几个常规的操作,如果想非常深入地理解文件系统的实现原理,请熟悉使用上述工具并阅读相关资料。
参考资料:
如何开发自己的文件系统
随着 fuse
的出现,在用户空间开发文件系统成为可能,如果想开发自己的文件系统,那么推荐阅读:使用 fuse 开发自己的文件系统。
进程操作
前言
进程作为程序真正发挥作用时的“形态”,我们有必要对它的一些相关操作非常熟悉,这一节主要描述进程相关的概念和操作,将介绍包括程序、进程、作业等基本概念以及进程状态查询、进程通信等相关的操作。
什么是程序,什么又是进程
程序是指令的集合,而进程则是程序执行的基本单元。为了让程序完成它的工作,必须让程序运行起来成为进程,进而利用处理器资源、内存资源,进行各种 I/O
操作,从而完成某项特定工作。
从这个意思上说,程序是静态的,而进程则是动态的。
进程有区别于程序的地方还有:进程除了包含程序文件中的指令数据以外,还需要在内核中有一个数据结构用以存放特定进程的相关属性,以便内核更好地管理和调度进程,从而完成多进程协作的任务。因此,从这个意义上可以说“高于”程序,超出了程序指令本身。
如果进行过多进程程序的开发,又会发现,一个程序可能创建多个进程,通过多个进程的交互完成任务。在 Linux 下,多进程的创建通常是通过 fork
系统调用来实现。从这个意义上来说程序则”包含”了进程。
另外一个需要明确的是,程序可以由多种不同程序语言描述,包括 C 语言程序、汇编语言程序和最后编译产生的机器指令等。
下面简单讨论 Linux 下面如何通过 Shell 进行进程的相关操作。
进程的创建
通常在命令行键入某个程序文件名以后,一个进程就被创建了。例如,
范例:让程序在后台运行
$ sleep 100 &
[1] 9298
范例:查看进程 ID
用pidof
可以查看指定程序名的进程ID:
$ pidof sleep
9298
范例:查看进程的内存映像
$ cat /proc/9298/maps
08048000-0804b000 r-xp 00000000 08:01 977399 /bin/sleep
0804b000-0804c000 rw-p 00003000 08:01 977399 /bin/sleep
0804c000-0806d000 rw-p 0804c000 00:00 0 [heap]
b7c8b000-b7cca000 r--p 00000000 08:01 443354
...
bfbd8000-bfbed000 rw-p bfbd8000 00:00 0 [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
程序被执行后,就被加载到内存中,成为了一个进程。上面显示了该进程的内存映像(虚拟内存),包括程序指令、数据,以及一些用于存放程序命令行参数、环境变量的栈空间,用于动态内存申请的堆空间都被分配好。
实际上,创建一个进程,也就是说让程序运行,还有其他的办法,比如,通过一些配置让系统启动时自动启动程序(具体参考 man init
),或者是通过配置 crond
(或者 at
)让它定时启动程序。除此之外,还有一个方式,那就是编写 Shell 脚本,把程序写入一个脚本文件,当执行脚本文件时,文件中的程序将被执行而成为进程。这些方式的细节就不介绍,下面了解如何查看进程的属性。
需要补充一点的是:在命令行下执行程序,可以通过 ulimit
内置命令来设置进程可以利用的资源,比如进程可以打开的最大文件描述符个数,最大的栈空间,虚拟内存空间等。具体用法见 help ulimit
。
查看进程的属性和状态
可以通过 ps
命令查看进程相关属性和状态,这些信息包括进程所属用户,进程对应的程序,进程对 cpu
和内存的使用情况等信息。熟悉如何查看它们有助于进行相关的统计分析等操作。
范例:通过 ps 命令查看进程属性
查看系统当前所有进程的属性:
$ ps -ef
查看命令中包含某字符的程序对应的进程,进程 ID
是 1 。 TTY
为?表示和终端没有关联:
$ ps -C init
PID TTY TIME CMD
1 ? 00:00:01 init
选择某个特定用户启动的进程:
$ ps -U falcon
按照指定格式输出指定内容,下面输出命令名和 cpu
使用率:
$ ps -e -o "%C %c"
打印 cpu
使用率最高的前 4 个程序:
$ ps -e -o "%C %c" | sort -u -k1 -r | head -5
7.5 firefox-bin
1.1 Xorg
0.8 scim-panel-gtk
0.2 scim-bridge
获取使用虚拟内存最大的 5 个进程:
$ ps -e -o "%z %c" | sort -n -k1 -r | head -5
349588 firefox-bin
96612 xfce4-terminal
88840 xfdesktop
76332 gedit
58920 scim-panel-gt
范例:通过 pstree 查看进程亲缘关系
系统所有进程之间都有“亲缘”关系,可以通过 pstree
查看这种关系:
$ pstree
上面会打印系统进程调用树,可以非常清楚地看到当前系统中所有活动进程之间的调用关系。
范例:用top动态查看进程信息
$ top
该命令最大特点是可以动态地查看进程信息,当然,它还提供了一些其他的参数,比如 -S
可以按照累计执行时间的大小排序查看,也可以通过 -u
查看指定用户启动的进程等。
补充: top
命令支持交互式,比如它支持 u
命令显示用户的所有进程,支持通过 k
命令杀掉某个进程;如果使用 -n 1
选项可以启用批处理模式,具体用法为:
$ top -n 1 -b
范例:确保特定程序只有一个副本在运行
下面来讨论一个有趣的问题:如何让一个程序在同一时间只有一个在运行。
这意味着当一个程序正在被执行时,它将不能再被启动。那该怎么做呢?
假如一份相同的程序被复制成了很多份,并且具有不同的文件名被放在不同的位置,这个将比较糟糕,所以考虑最简单的情况,那就是这份程序在整个系统上是唯一的,而且名字也是唯一的。这样的话,有哪些办法来回答上面的问题呢?
总的机理是:在程序开头检查自己有没有执行,如果执行了则停止否则继续执行后续代码。
策略则是多样的,由于前面的假设已经保证程序文件名和代码的唯一性,所以通过 ps
命令找出当前所有进程对应的程序名,逐个与自己的程序名比较,如果已经有,那么说明自己已经运行了。
ps -e -o "%c" | tr -d " " | grep -q ^init$ #查看当前程序是否执行
[ $? -eq 0 ] && exit #如果在,那么退出, $?表示上一条指令是否执行成功
每次运行时先在指定位置检查是否存在一个保存自己进程 ID
的文件,如果不存在,那么继续执行,如果存在,那么查看该进程 ID
是否正在运行,如果在,那么退出,否则往该文件重新写入新的进程 ID
,并继续。
pidfile=/tmp/$0".pid"
if [ -f $pidfile ]; then
OLDPID=$(cat $pidfile)
ps -e -o "%p" | tr -d " " | grep -q "^$OLDPID$"
[ $? -eq 0 ] && exit
fi
echo $$ > $pidfile
#... 代码主体
#设置信号0的动作,当程序退出时触发该信号从而删除掉临时文件
trap "rm $pidfile" 0
更多实现策略自己尽情发挥吧!
调整进程的优先级
在保证每个进程都能够顺利执行外,为了让某些任务优先完成,那么系统在进行进程调度时就会采用一定的调度办法,比如常见的有按照优先级的时间片轮转的调度算法。这种情况下,可以通过 renice
调整正在运行的程序的优先级,例如:`
范例:获取进程优先级
$ ps -e -o "%p %c %n" | grep xfs
5089 xfs 0
范例:调整进程的优先级
$ renice 1 -p 5089
renice: 5089: setpriority: Operation not permitted
$ sudo renice 1 -p 5089 #需要权限才行
[sudo] password for falcon:
5089: old priority 0, new priority 1
$ ps -e -o "%p %c %n" | grep xfs #再看看,优先级已经被调整过来了
5089 xfs 1
结束进程
既然可以通过命令行执行程序,创建进程,那么也有办法结束它。可以通过 kill
命令给用户自己启动的进程发送某个信号让进程终止,当然“万能”的 root
几乎可以 kill
所有进程(除了 init
之外)。例如,
范例:结束进程
$ sleep 50 & #启动一个进程
[1] 11347
$ kill 11347
kill
命令默认会发送终止信号( SIGTERM
)给程序,让程序退出,但是 kill
还可以发送其他信号,这些信号的定义可以通过 man 7 signal
查看到,也可以通过 kill -l
列出来。
$ man 7 signal
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
范例:暂停某个进程
例如,用 kill
命令发送 SIGSTOP
信号给某个程序,让它暂停,然后发送 SIGCONT
信号让它继续运行。
$ sleep 50 &
[1] 11441
$ jobs
[1]+ Running sleep 50 &
$ kill -s SIGSTOP 11441 #这个等同于我们对一个前台进程执行CTRL+Z操作
$ jobs
[1]+ Stopped sleep 50
$ kill -s SIGCONT 11441 #这个等同于之前我们使用bg %1操作让一个后台进程运行起来
$ jobs
[1]+ Running sleep 50 &
$ kill %1 #在当前会话(session)下,也可以通过作业号控制进程
$ jobs
[1]+ Terminated sleep 50
可见 kill
命令提供了非常好的功能,不过它只能根据进程的 ID
或者作业来控制进程,而 pkill
和 killall
提供了更多选择,它们扩展了通过程序名甚至是进程的用户名来控制进程的方法。更多用法请参考它们的手册。
范例:查看进程退出状态
当程序退出后,如何判断这个程序是正常退出还是异常退出呢?还记得 Linux 下,那个经典 hello world
程序吗?在代码的最后总是有条 return 0
语句。这个 return 0
实际上是让程序员来检查进程是否正常退出的。如果进程返回了一个其他的数值,那么可以肯定地说这个进程异常退出了,因为它都没有执行到 return 0
这条语句就退出了。
那怎么检查进程退出的状态,即那个返回的数值呢?
在 Shell
中,可以检查这个特殊的变量 $?
,它存放了上一条命令执行后的退出状态。
$ test1
bash: test1: command not found
$ echo $?
127
$ cat ./test.c | grep hello
$ echo $?
1
$ cat ./test.c | grep hi
printf("hi, myself!\n");
$ echo $?
0
貌似返回 0 成为了一个潜规则,虽然没有标准明确规定,不过当程序正常返回时,总是可以从 $?
中检测到 0,但是异常时,总是检测到一个非 0 值。这就告诉我们在程序的最后最好是跟上一个 exit 0
以便任何人都可以通过检测 $?
确定程序是否正常结束。如果有一天,有人偶尔用到你的程序,试图检查它的退出状态,而你却在程序的末尾莫名地返回了一个 -1
或者 1,那么他将会很苦恼,会怀疑他自己编写的程序到底哪个地方出了问题,检查半天却不知所措,因为他太信任你了,竟然从头至尾都没有怀疑你的编程习惯可能会与众不同!
进程通信
为便于设计和实现,通常一个大型的任务都被划分成较小的模块。不同模块之间启动后成为进程,它们之间如何通信以便交互数据,协同工作呢?在《UNIX 环境高级编程》一书中提到很多方法,诸如管道(无名管道和有名管道)、信号(signal
)、报文(Message
)队列(消息队列)、共享内存(mmap/munmap
)、信号量(semaphore
,主要是同步用,进程之间,进程的不同线程之间)、套接口(Socket
,支持不同机器之间的进程通信)等,而在 Shell 中,通常直接用到的就有管道和信号等。下面主要介绍管道和信号机制在 Shell 编程时的一些用法。
范例:无名管道(pipe)
在 Linux 下,可以通过 |
连接两个程序,这样就可以用它来连接后一个程序的输入和前一个程序的输出,因此被形象地叫做个管道。在 C 语言中,创建无名管道非常简单方便,用 pipe
函数,传入一个具有两个元素的 int
型的数组就可以。这个数组实际上保存的是两个文件描述符,父进程往第一个文件描述符里头写入东西后,子进程可以从第一个文件描述符中读出来。
如果用多了命令行,这个管子 |
应该会经常用。比如上面有个演示把 ps
命令的输出作为 grep
命令的输入:
$ ps -ef | grep init
也许会觉得这个“管子”好有魔法,竟然真地能够链接两个程序的输入和输出,它们到底是怎么实现的呢?实际上当输入这样一组命令时,当前 Shell 会进行适当的解析,把前面一个进程的输出关联到管道的输出文件描述符,把后面一个进程的输入关联到管道的输入文件描述符,这个关联过程通过输入输出重定向函数 dup
(或者 fcntl
)来实现。
范例:有名管道(named pipe)
有名管道实际上是一个文件(无名管道也像一个文件,虽然关系到两个文件描述符,不过只能一边读另外一边写),不过这个文件比较特别,操作时要满足先进先出,而且,如果试图读一个没有内容的有名管道,那么就会被阻塞,同样地,如果试图往一个有名管道里写东西,而当前没有程序试图读它,也会被阻塞。下面看看效果。
$ mkfifo fifo_test #通过mkfifo命令创建一个有名管道
$ echo "fewfefe" > fifo_test
#试图往fifo_test文件中写入内容,但是被阻塞,要另开一个终端继续下面的操作
$ cat fifo_test #另开一个终端,记得,另开一个。试图读出fifo_test的内容
fewfefe
这里的 echo
和 cat
是两个不同的程序,在这种情况下,通过 echo
和 cat
启动的两个进程之间并没有父子关系。不过它们依然可以通过有名管道通信。
这样一种通信方式非常适合某些特定情况:例如有这样一个架构,这个架构由两个应用程序构成,其中一个通过循环不断读取 fifo_test
中的内容,以便判断,它下一步要做什么。如果这个管道没有内容,那么它就会被阻塞在那里,而不会因死循环而耗费资源,另外一个则作为一个控制程序不断地往 fifo_test
中写入一些控制信息,以便告诉之前的那个程序该做什么。下面写一个非常简单的例子。可以设计一些控制码,然后控制程序不断地往 fifo_test
里头写入,然后应用程序根据这些控制码完成不同的动作。当然,也可以往 fifo_test
传入除控制码外的其他数据。
应用程序的代码
$ cat app.sh #!/bin/bash FIFO=fifo_test while :; do CI=`cat $FIFO` #CI --> Control Info case $CI in 0) echo "The CONTROL number is ZERO, do something ..." ;; 1) echo "The CONTROL number is ONE, do something ..." ;; *) echo "The CONTROL number not recognized, do something else..." ;; esac done
控制程序的代码
$ cat control.sh #!/bin/bash FIFO=fifo_test CI=$1 [ -z "$CI" ] && echo "the control info should not be empty" && exit echo $CI > $FIFO
一个程序通过管道控制另外一个程序的工作
$ chmod +x app.sh control.sh #修改这两个程序的可执行权限,以便用户可以执行它们 $ ./app.sh #在一个终端启动这个应用程序,在通过./control.sh发送控制码以后查看输出 The CONTROL number is ONE, do something ... #发送1以后 The CONTROL number is ZERO, do something ... #发送0以后 The CONTROL number not recognized, do something else... #发送一个未知的控制码以后 $ ./control.sh 1 #在另外一个终端,发送控制信息,控制应用程序的工作 $ ./control.sh 0 $ ./control.sh 4343
这样一种应用架构非常适合本地的多程序任务设计,如果结合 web cgi
,那么也将适合远程控制的要求。引入 web cgi
的唯一改变是,要把控制程序 ./control.sh
放到 web
的 cgi
目录下,并对它作一些修改,以使它符合 CGI
的规范,这些规范包括文档输出格式的表示(在文件开头需要输出 content-tpye: text/html
以及一个空白行)和输入参数的获取 (web
输入参数都存放在 QUERY_STRING
环境变量里头)。因此一个非常简单的 CGI
控制程序可以写成这样:
#!/bin/bash
FIFO=./fifo_test
CI=$QUERY_STRING
[ -z "$CI" ] && echo "the control info should not be empty" && exit
echo -e "content-type: text/html\n\n"
echo $CI > $FIFO
在实际使用时,请确保 control.sh
能够访问到 fifo_test
管道,并且有写权限,以便通过浏览器控制 app.sh
:
http://ipaddress\_or\_dns/cgi-bin/control.sh?0
问号 ?
后面的内容即 QUERY_STRING
,类似之前的 $1
。
这样一种应用对于远程控制,特别是嵌入式系统的远程控制很有实际意义。在去年的暑期课程上,我们就通过这样一种方式来实现马达的远程控制。首先,实现了一个简单的应用程序以便控制马达的转动,包括转速,方向等的控制。为了实现远程控制,我们设计了一些控制码,以便控制马达转动相关的不同属性。
在 C 语言中,如果要使用有名管道,和 Shell 类似,只不过在读写数据时用 read
,write
调用,在创建 fifo
时用 mkfifo
函数调用。
范例:信号(Signal)
信号是软件中断,Linux 用户可以通过 kill
命令给某个进程发送一个特定的信号,也可以通过键盘发送一些信号,比如 CTRL+C
可能触发 SGIINT
信号,而 CTRL+\
可能触发 SGIQUIT
信号等,除此之外,内核在某些情况下也会给进程发送信号,比如在访问内存越界时产生 SGISEGV
信号,当然,进程本身也可以通过 kill
,raise
等函数给自己发送信号。对于 Linux 下支持的信号类型,大家可以通过 man 7 signal
或者 kill -l
查看到相关列表和说明。
对于有些信号,进程会有默认的响应动作,而有些信号,进程可能直接会忽略,当然,用户还可以对某些信号设定专门的处理函数。在 Shell 中,可以通过 trap
命令(Shell 内置命令)来设定响应某个信号的动作(某个命令或者定义的某个函数),而在 C 语言中可以通过 signal
调用注册某个信号的处理函数。这里仅仅演示 trap
命令的用法。
$ function signal_handler { echo "hello, world."; } #定义signal_handler函数
$ trap signal_handler SIGINT #执行该命令设定:收到SIGINT信号时打印hello, world
$ hello, world #按下CTRL+C,可以看到屏幕上输出了hello, world字符串
类似地,如果设定信号 0 的响应动作,那么就可以用 trap
来模拟 C 语言程序中的 atexit
程序终止函数的登记,即通过 trap signal_handler SIGQUIT
设定的 signal_handler
函数将在程序退出时执行。信号 0 是一个特别的信号,在 POSIX.1
中把信号编号 0 定义为空信号,这常被用来确定一个特定进程是否仍旧存在。当一个程序退出时会触发该信号。
$ cat sigexit.sh
#!/bin/bash
function signal_handler {
echo "hello, world"
}
trap signal_handler 0
$ chmod +x sigexit.sh
$ ./sigexit.sh #实际Shell编程会用该方式在程序退出时来做一些清理临时文件的收尾工作
hello, world
作业和作业控制
当我们为完成一些复杂的任务而将多个命令通过 |,\>,<, ;, (,)
等组合在一起时,通常这个命令序列会启动多个进程,它们间通过管道等进行通信。而有时在执行一个任务的同时,还有其他的任务需要处理,那么就经常会在命令序列的最后加上一个&,或者在执行命令后,按下 CTRL+Z
让前一个命令暂停。以便做其他的任务。等做完其他一些任务以后,再通过 fg
命令把后台任务切换到前台。这样一种控制过程通常被成为作业控制,而那些命令序列则被成为作业,这个作业可能涉及一个或者多个程序,一个或者多个进程。下面演示一下几个常用的作业控制操作。
范例:创建后台进程,获取进程的作业号和进程号
$ sleep 50 &
[1] 11137
范例:把作业调到前台并暂停
使用 Shell 内置命令 fg
把作业 1 调到前台运行,然后按下 CTRL+Z
让该进程暂停
$ fg %1
sleep 50
^Z
[1]+ Stopped sleep 50
范例:查看当前作业情况
$ jobs #查看当前作业情况,有一个作业停止
[1]+ Stopped sleep 50
$ sleep 100 & #让另外一个作业在后台运行
[2] 11138
$ jobs #查看当前作业情况,一个正在运行,一个停止
[1]+ Stopped sleep 50
[2]- Running sleep 100 &
范例:启动停止的进程并运行在后台
$ bg %1
[2]+ sleep 50 &
不过,要在命令行下使用作业控制,需要当前 Shell,内核终端驱动等对作业控制支持才行。
网络操作
前言
前面章节已经介绍了Shell编程范例之数值、布尔值、字符串、文件、文件系统、进程等的操作。这些内容基本覆盖了网络中某个独立机器正常工作的“方方面面”,现在需要把视角从单一的机器延伸到这些机器通过各种网络设备和协议连接起来的网络世界,分析网络拓扑结构、网络工作原理、了解各种常见网络协议、各种常见硬件工作原理、网络通信与安全相关软件以及工作原理分析等。
不过,因为网络相关的问题确实太复杂了,这里不可能介绍具体,因此如果想了解更多细节,还是建议参考相关资料。但Linux是一个网络原理学习和实践的好平台,不仅因为它本身对网络体系结构的实现是开放源代码的,而且各种相关的分析工具和函数库数不胜数,因此千万不要错过通过它来做相关的实践工作。
网络原理介绍
我们的网络世界
在进行所有介绍之前,来直观地感受一下那个真真实实存在的网络世界吧。当我在 Linux 下通过 Web
编辑器写这篇 Blog 时,一边用 mplayer
听着远程音乐,累了时则打开兰大的网络 TV
频道开始看看凤凰卫视……这些“现代化”的生活,我想,如果没有网络,将变得无法想象。
下面来构想一下这样一个网络世界的优美图画:
一边盯着显示器,一边敲击着键盘,一边挂着耳机。
主机电源灯灿烂得很,发着绿光,这时很容易想象主机背后的那个网卡位置肯定有两个不同颜色的灯光在闪烁,它显示着主机正在与计算机网络世界打着交道。
就在实验室的某个角落,有一个交换机上的一个网口的网线连到主机上,这个交换机接到了一个局域网的网关上,然后这个网关再接到了信息楼的某个路由器上,再转接到学校网络中心的另外一个路由器上……
期间,有一个路由器连接到了这个 Blog 服务器上,而另外一个则可能连到了那个网络
TV
服务器上,还有呢,另外一些则连接到了电信网络里头的某个音乐服务器上……
下面用 dia
绘制一个简单的“网络地图”:
该图把一些最常见的网络设备和网络服务基本都呈现出来了,包括本地主机、路由、交换机、网桥,域名服务器,万维网服务,视频服务,防火墙服务,动态 IP
地址服务等。其中各种设备构成了整个物理网络,而网络服务则是构建在这些设备上的各种网络应用。
现在的网络应用越来越丰富多样,比如即时聊天(IM
)、 p2p
资源共享、网络搜索等,它们是如何实现的,它们如何构建在各种各样的网络设备之上,并且能够安全有效的工作呢?这取决于这背后逐步完善的网络体系结构和各种相关网络协议的开发、实现和应用。
网络体系结构和网络协议介绍
那么网络体系结构是怎么样的呢?涉及到哪些相关的网络协议呢?什么又是网络协议呢?
在《计算机网络——自顶向下的方法》一书中非常巧妙地给出了网络体系结构分层的比喻,把网络中各层跟交通运输体系中的各个环节对照起来,让人通俗易懂。在交通运输体系中,运输的是人和物品,在计算机网络体系中,运输的是电子数据。考虑到交通运输网络和计算机网络中最终都可以划归为点对点的信息传输。这里考虑两点之间的信息传递过程,得到这样一个对照关系,见下图:
对照上图,更容易理解右侧网络体系结构的分层原理(如果比照一封信发出到收到的这一中间过程可能更容易理解),上图右侧是 TCP/IP
网络体系结构的一个网络分层示意图,在把数据发送到网络之前,在各层中需要进行各种“打包”的操作,而从网络接收到数据后,就需要进行“解包”操作,最终把纯粹的数据信息给提取出来。这种分层的方式是为了传输数据的需要,也是两个主机之间如何建立连接以及如何保证数据传输的完整性和可靠性的需要。通过把各种需要分散在不同的层次,使得整个体系结构更加清晰和明了。这些“需求”具体通过各种对应的协议来规范,这些规范统成为网络协议。
关于 OSI
模型(7 层)比照 TCP/IP
模型(4 层)的协议栈可以从下图(来自网络)看个明了:
而下图(来自网络)则更清晰地体现了 TCP/IP
分层模型。
上面介绍了网络原理方面的基本内容,如果想了解更多网络原理和操作系统对网络支持的实现,可以考虑阅读后面的参考资料。下面将做一些实践,即在 Linux 下如何联网,如何用 Linux 搭建各种网络服务,并进行网络安全方面的考量以及基本的网络编程和开发的介绍。
Linux 下网络“实战”
如何把我们的 Linux 主机接入网络
如果要让一个系统能够联网,首先当然是搭建好物理网络了。接入网络的物理方式还是蛮多的,比如直接用网线接入以太网,用无线网卡上网,用 ADSL
拨号上网……
对于用以太网网卡接入网络的常见方式,在搭建好物理网络并确保连接正常后,可以通过配置 IP
地址和默认网关来接入网络,这个可以通过手工配置和动态获取两种方式。
范例:通过dhclient获取IP地址
如果所在的局域网有 DHCP
服务,那么可以这么获取,N
是设备名称,如果只有一块网卡,一般是 0 或者 1 。
$ dhclient ethN
范例:静态配置IP地址
当然,也可以考虑采用静态配置的方式,ip_address
是本地主机的 IP
地址,gw_ip_address
是接入网络的网关的 IP
地址。
$ ifconfig eth0 ip_address on
$ route add deafult gw gw_ip_address
如果上面不工作,记得通过 ifconfig/mii-tool/ethtool
等工具检查网卡是否有被驱动起来,然后通过 lspci/dmesg
等检查网卡类型(或者通过主板手册和独立网卡自带的手册查看),接着安装或者编译相关驱动,最后把驱动通过 insmod/modprobe
等工具加载到内核中。
用 Linux 搭建网桥
网桥工作在 OSI
模型的第二层,即数据链路层,它只需要知道目标主机的 MAC
地址就可以工作。 Linux 内核在 2.2
开始就已经支持了这个功能,具体怎么配置看看后续参考资料吧。如果要把 Linux 主机配置成一个网桥,至少需要两个网卡。
网桥的作用相当于一根网线,用户无须关心里头有什么东西,把它的两个网口连接到两个主机上就可以让这两个主机支持相互通信。不过它比网线更厉害,如果配上防火墙,就可以隔离连接在它两端的网段(注意这里是网络,因为它不识别 IP
),另外,如果这个网桥有多个网口,那么可以实现一个功能复杂的交换机,而如果有效组合多个网桥,则有可能实现一个复杂的可实现流量控制和负载平衡的防火墙系统。
用 Linux 做路由
路由工作在 OSI
模型的第三层,即网络层,通过 router
可以配置 Linux 的路由,当然,Linux 下也有很多工具支持动态路由的。相关的资料在网路中铺天盖地,由于时间关系,这里不做介绍。
用 Linux 搭建各种常规的网络服务
需要什么网络服务呢?
- 给局域网弄个
DHCP
服务器,那就弄个dhcpd
,看看参考资料; - 如果想弄个邮件发送服务器,那就安装个
sendmail
或者exim4
; - 如果再想弄个邮件列表服务器呢,那就装个
mailman
; - 如果想弄个接收邮件的服务器呢,那就安装个
pop3
服务器; - 如果想弄个
web
站点,那就弄个apache
或者nginx
服务器; - 如果想弄上防火墙服务,那么通过
iptables
工具配置netfilter
就可以
What’s more?如果你能想到,Linux上基本都有相应的实现。
Linux 下网络问题诊断与维护
如果出现网络问题,不要惊慌,逐步检查网络的各个层次:物理链接、链路层、网络层直到应用层,熟悉使用各种如下的工具,包括 ethereal/tcpdump
,hping
,nmap
,netstat
,netpipe
,netperf
,vnstat
,ntop
等。
关于这些工具的详细用法和网络问题诊断和维护的相关知识,请看后续相关资料。
Linux 下网络编程与开发
如果想做网络编程开发,比如:
- 要实现一个客户端
/
服务器架构的应用,可以采用 Linux 下的socket
编程了; - 如果想写一个数据包抓获和协议分析的程序,可以采用
libpap
等函数库; - 如果想实现某个协议呢,那就可以参考相关的
RFC
文档,并通过socket
编程来实现。
这个可以参考相关的 Linux socket
编程等资料。
后记
本来介绍网络相关的一些基本内容,但因时间关系,没有详述,更多细节请参考相关资料。
参考资料
- 计算机网络——自上而下的分析方法
- Linux 网络体系结构(清华大学出版社出版)
- Linux 系统故障诊断与排除 第13章 网络问题(人民邮电出版社)
- 在 Linux 下用 ADSL 拨号上网
- Linux 下无线网络相关资料收集
- Linux网桥的实现分析与使用
- DHCP mini howto
- 最佳的 75 个安全工具
- 网络管理员必须掌握的知识
- Linux 上检测 rootkit 的两种工具: Rootkit Hunter 和 Chkrootkit
- 数据包抓获与 ip 协议的简单分析(基于 pcap 库)
- RFC
- HTTP 协议的 C 语言编程实现实例
用户管理
在实际使用中,Linux 系统首先是面向用户的系统,所有之前介绍的内容全部是提供给不同的用户使用的。实际使用中常常碰到各类用户操作,所以这里添加一个独立的章节来介绍。
Linux 支持多用户,也就是说允许不同的人使用同一个系统,每个人有一个属于自己的帐号。而且允许大家设置不同的认证密码,确保大家的私有信息得到保护。另外,为了确保整个系统的安全,用户权限又做了进一步划分,包括普通用户和系统管理员。普通用户只允许访问自己账户授权下的信息,而系统管理员才能访问所有资源。普通用户如果想行使管理员的职能,必须获得系统管理员的许可。
为避免分散注意力,咱们不去介绍背后的那些数据文件: /etc/passwd
,/etc/shadow
,/etc/group
,/etc/gshadow
如果确实有需要,大家可通过如下命令查看帮助: man 5 passwd
,man shadow
, man group
和 man gshadow
下面我们分如下几个部分来介绍:
- 用户帐号
- 用户口令
- 用户组别
- 用户和组
- 用户切换
用户帐号
帐号操作主要是增、删、改、禁。Linux 系统提供了底层的 useradd
, userdel
和 usermod
来完成相关操作,也提供了进一步的简化封装:adduser
, deluser
。为了避免混淆,咱们这里只介绍最底层的指令,这些指令设计上已经够简洁明了方便。
由于只有系统管理员才能创建新用户,请确保以 root 帐号登录或者可以通过 sudo 切换为管理员帐号。
添加
创建家目录并指定登录 Shell:
# useradd -s /bin/bash -m test
# groups test
test : test
并加入所属组:
# useradd -s /bin/bash -m -G docker test
# groups test
test : test docker
删除
删除用户以及家目录等:
# userdel -r test
修改
常常用来修改默认的 Shell:
# usermod -s /bin/bash test
或者把用户加入某个新安装软件所属的组:
# usermod -a -G docker test
修改登录用户名并搬到新家:
# usermod -d /home/new_test -m -l new_test test
禁用
如果想禁用某个帐号:
# usermod -L test
# usermod --expiredate 1 test
用户口令
口令操作主要是设置、删除、修改和禁用。Linux 系统提供了 passwd
命令来管理用户口令。
设置
设置用户 test 的初始密码:
$ passwd test
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
删除
让用户 test 无须密码登录(密码为空):
$ passwd -d test
这个很方便某些安全无关紧要的条件下(比如已登录主机中的虚拟机),可避免每次频繁输入密码。
修改
$ passwd test
Changing password for test.
(current) UNIX password:
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
禁用
禁止用户通过密码登录:
$ passwd -l user
为了安全起见或者为了避免暴力破解,我们通常可以禁用密码登录,而只允许通过 SSH Key 登录。
如果要真地禁用整个帐号的使用,需要用上一节提到的 usermod --expiredate 1
。
用户组别
类似帐号,主要操作也是增、删、改。
Linux 系统提供了底层的 groupadd
, groupdel
和 groupmod
来完成相关操作,也提供了进一步的简化封装:addgroup
, delgroup
。
用户组别通常用来管理不同的资源,确保只有某个组别的用户才可以访问某类资源。当然,实际案例中,有些软件也为自己定义一个组别,只有该组别的用户才能访问该软件的一些功能。
添加
添加一个新组别:
# groupadd test
删除
# groupdel test
修改
修改组别名:
# groupmod -n new_test test
用户和组
用户和组别不能独立存在,gpasswd
可以用来处理两者的关系。
增加
从 docker 组中增加用户 test(等同于把 test 增加到 docker 组中):
# gpasswd -a test docker
或
# usermod -a -G docker test
删除
从 test 组中删除用户 test:
# gpasswd -d test test
用户切换
由于支持多用户,那么在登录一个帐号后,可能需要切换到另外一个帐号下,可以通过 su
命令完成,而 sudo
则可以用来作为另外一个用户来执行命令。
切换帐号
切换到 Root 并启用 Bash:
$ su -s /bin/bash -
root@falcon-desktop:~#
或者
$ sudo -s
切换到普通用户:
$ su -s /bin/bash - test
test@falcon-desktop:~$
或者
$ sudo -i -u test
test@falcon-desktop:~$
免密码切到 Root
首先得把用户加入到 sudo 用户组:
# usermod -a -G sudo falcon
否则,会看到如下信息:
$ sudo -s
[sudo] password for test:
test is not in the sudoers file. This incident will be reported.
加入 sudo 用户组以后:
$ sudo -s
[sudo] password for test:
要实现免密切换,需要先修改 /etc/sudoers
,加入如下一行:
test ALL=(ALL) NOPASSWD: ALL
或者在 /etc/sudoers.d/
下创建一个文件并加入上述内容。
# echo "test ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/test
# chmod 440 /etc/sudoers.d/test
总结
到这里,整个复习系列就要结束了,作为总结篇,主要回顾一下各个小节的主要内容,并总结出 Shell 编程的一些常用框架和相关注意事项等。
正确使用 source
和 .
仅使用 source
和 .
来执行你的环境配置等功能,建议不要用于其它用途。 在Shell中使用脚本时,使用 bash your_script.sh
而不是 source your_script.sh
或 . your_script.sh
。
当使用 bash
的时候,当前的Shell会创建一个新的子进程执行你的脚本;当使用 source
和 .
时,当前的Shell会直接解释执行 your_script.sh
中的代码。如果 your_script.sh
中包含了类似 exit 0
这样的代码,使用source
和 .
执行会导致当前Shell意外地退出。