1 Linux系统的设计优点
上个世纪六十年代,贝尔实验室Ken Thompson发明了UNIX,Dennis Ritchie参与了开发。在吸取了远古 Multics 操作系统研发中的失败经验后,里奇将UNIX的设计原则定为Keep it simple & stupid(大道至简)。
UNIX 的特点:
- 所有的事物(甚至硬件本身)都是一个的文件,Everything (including hardware) is a file
- 以文本形式储存配置数据,Configuration data stored in text
- 程序尽量朝向小而单一的目标设计,Small, single-purpose program
-尽量避免令人困惑的用户接口, Avoid captive user interfaces
- 将几个程序连接起来,处理大而复杂的工作,Ability to chain program together to perform complex tasks
这几条设计思路,可以说每一条都是伟大的创举。尤其是第一条“所有的事物(甚至硬件本身)都是一个的文件”,实在是强大无比、妙不可言。
2Linux下一切皆文件
“一切皆是文件”是 Unix/Linux 的基本哲学之一。
在 Linux/UNIX 操作系统中,所有事物都被当作文件来处理:硬件设备(包括键盘和终端)、目录、命令本身,当然还有磁盘文件。这个奇怪的惯例实际上是 Linux/UNIX 的能力和灵活性的基础。
首先,在windows中是文件的东西,比如一份txt文档、一个WORD文档、或者一个exe程序等等能存在磁盘上的有名字的东西,它们在linux中也是文件(当然windows上的exe程序文件不能在Linux上面运行)。
其次一些在windows中不是文件的东西。第一类,屏幕、磁盘、串口、USB、鼠标、键盘等有形硬件设备,被抽象成了文件;运行时产生的内核对象,比如进程、标准错误(stderr)等,也被抽象成了文件;甚至有一些很离谱的东西,比如管道、socket(套接字),等等也是文件。
这样做的缺点就是:概念太难以理解了。但是,一旦思维转变过来之后,就可以享受到“一切皆是文件”无穷无尽的好处。
最明显的好处是,开发者仅需要使用一套 API 即可调取 Linux 系统中绝大部分的资源。举个简单的例子,在Linux 中几乎所有读(读文件,读系统状态,读 socket,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写 socket,写 PIPE)的操作都可以用write函数来进行。
另外一个好处是,极大地减少了系统编程所需用的API数量,降低了系统编程的复杂度。我看到一个2009年汇总的WIN API大全,已经2248个了,此后还在不断增加之中。(传言现在已经高达上万了,不过我懒得考证。)
当然,一点不利之处是:使用任何硬件设备都必须在根目录下某一目录执行挂载操作,否则无法使用。我们知道,本身Linux具有一个以根目录为树根的文件目录结构,每个设备也同样如此,它们是相互独立的。如果我们想通过Linux上的根目录找到设备文件的目录结构,就必须将这两个文件系统目录合二为一,这就是挂载的真正含义。
没有完美的设计,对开发者来说,不利之处极少,有利之处极多。程序开发时,对大部分设备、资源的操作可以统一接口,甚至只用
open/read/write/ioctl/close就可以完成大部分(甚至全部)操作。对这些资源的访问代码、基本操作都是高度相似的,代码的移植也更加方便。
3Linux平台下的文件类型
当然,如果要汇总一下,Linux平台上的文件主要可以有6种分类。
1) 普通文件
类似 mp4、pdf、html 这样,可直接拿来使用的文件都属于普通文件,Linux 用户根据访问权限的不同可以对这些文件进行查看、删除以及更改操作。
2) 目录文件
对于用惯 Windows 系统的用户来说,目录是文件可能不太好理解。
Linux 系统中,目录文件包含了此目录中各个文件的文件名以及指向这些文件的指针,打开目录等同于打开目录文件,只要你有权限,可以随意访问目录中的任何文件。
注意,目录文件的访问权限,同普通文件的执行权限,意思一样。
3) 字符设备文件和块设备文件
这些文件通常隐藏在 /dev/ 目录下,当进行设备读取或外设交互时才会被使用。
例如,磁盘光驱属于块设备文件,串口设备则属于字符设备文件。
Linux 系统中的所有设备,要么是块设备文件,要么是字符设备文件。
4) 套接字文件(socket)
套接字文件一般隐藏在 /var/run/ 目录下,用于进程间的网络通信。
5) 符号链接文件(symbolic link)
类似于 Windows 中的快捷方式,是指向另一文件的简介指针(也就是软链接)。
6) 管道文件(pipe)
主要用于进程间通信。例如,使用 mkfifo 命令创建一个 FIFO 文件,与此同时,启用进程 A 从 FIFO文件读数据,启用进程 B 从 FIFO文件中写数据,随写随读。
4虚拟文件系统VFS
“一切皆是文件”是 Unix/Linux的设计哲学,但具体实现离不开虚拟文件系统VFS。
虚拟文件系统使得Linux可以并存多个“实际的文件系统”,比如分区1是ext3,分区2是ext4,分区3是NTFS。ext3、ext4和NTFS,它们每个“实际的文件系统”的结构和操作方式都是不一样的。如果是这样的话,用户怎么去操作它们呢?总不能每种不同的文件系统都采用不同的方法吧,那么这个时候就需要VFS作为中间层!用户直接控制VFS,VFS再去控制各个文件系统。
VFS存在的意义,可总结为以下四点:
(1)对application应用层提供标准一致的文件操作接口;
(2)对文件系统提供一个标准的接口,以便其他操作系统的文件系统可以方便地移植到Linux上;
(3)VFS内部则通过一系列高效的管理机制,比如inode cache, dentry cache 以及文件系统的预读等技术,使得底层文件系统不需沉溺到复杂的内核操作,即可获得高性能;
(4)此外VFS把一些复杂的操作尽量抽象到VFS内部,使得底层文件系统实现更简单。
5库函数与系统调用函数的关系
统调用,我们可以理解是操作系统为用户提供的一系列操作的接口(API),这些接口提供了对系统内核、硬件设备功能的操作。这么说可能会比较抽象,举个例子,我们最熟悉的 hello world 程序会在屏幕上打印出信息。程序中调用了 printf() 函数,而库函数 printf 本质上是调用了系统调用 write() 函数,实现了终端信息的打印功能。
库函数可以理解为是对系统调用的一层封装。可以是C库,也可以是C++库。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更方便于高效编程。这个封装的代码库就是C/C++库。
库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。
上图可以形象地用来理解Linux平台上面的C库函数与系统调用的关系。
《C 专家编程》书籍中的附录 A.4,书中关于两者区别的回答是这样的:函数库调用是语言或应用程序的一部分,而系统调用是操作系统的一部分。
- 所有 C 函数库是相同的,而各个操作系统的系统调用是不同的。
- C函数库调用是调用函数库中的一个程序,而系统调用是调用系统内核的服务。
- C函数库调用是与用户程序相联系,而系统调用是操作系统的一个进入点
- C函数库调用是在用户地址空间执行,而系统调用是在内核地址空间执行
- C函数库调用的运行时间属于「用户」时间,而系统调用的运行时间属于「系统」时间
- C函数库调用属于过程调用,开销较小,而系统调用需要切换到内核上下文环境然后切换回来,开销较大
- 在C函数库libc中大约 300 个程序,在 UNIX 中大约有 90 个系统调用
- 函数库典型的 C 函数:fopen,fread,fwrite,system, fprintf, malloc等等,而典型的系统调用:open,read,write,chdir, fork, write, brk等等。
6 Linux系统调用的作用
避免了用户直接对底层硬件进行编程。比如最简单的hello world程序是将信息打印到终端,终端对系统来说是硬件资源,如果没有系统调用,用户程序需要自己编写终端设备的驱动,以及控制终端如何显示的代码。
隐藏背后的技术细节。比如读写文件,如果使用了系统调用,用户程序无须关心数据在磁盘的哪个磁道和扇区,以及数据要加载到内存什么位置。
保证系统的安全性和稳定性。要知道用户程序是不能直接操作内核地址空间的,比如让一个新手直接编程访问内核底层的数据,那么内核系统的安全性就无法保证。而系统调用的功能是由内核来实现,用户只需要调用接口,无需关心细节,也避免了系统的安全隐患。
方便程序的可移植性。如果针对一个系统资源的功能操作比如 read(),大家都按照自己思路去实现这个功能,那么我们写出来的程序的移植性就会非常差。
7 通用I/O模型
I/O就是Input/Output,输入/输出。
既然Linux“一切皆是文件”,那么对文件的读写操作也是通用的,我们称之为通用I/O,也可以称之为文件I/O。在Linux平台上,文件I/O是一个通用性的概念,它要借用系统API调用的诸多接口函数(open/read/write...)来实现。