一. 文件结构
Linux 中的任何事物都可以用一个文件来表示,或者通过特殊的文件提供。更多细节请参考:http://blog.youkuaiyun.com/wellmikelan/article/details/7683186
常见的文件类型包括:
1. 普通文件
最常用的文件类型,包含了某种形式的数据,至于这种数据是文本还是还是二进制数据,对 Linux 内核而言并无区别。
2. 目录文件
这种文件包含了其它文件的名字以及指向与这些文件有关信息的指针,对一个目录文件具有读权限的任一进程都可以读取该目录的内容,但只有内核可以直接写目录文件。
3. 块特殊文件/块设备
这种文件类型提供对设备如磁盘带缓冲的访问,每次访问以固定长度为单位进行。
4. 字符特殊文件/字符设备
这种文件类型提供对设备如键盘、打印机不带缓冲的访问,以字符为单位进行传输,每次访问的长度可变。注意字符并不代表字节。
块文件和字符文件的基本区别是:块特殊文件包含被编号的块,每一块都可以独立地读取或者写入,而且可以定位于任何块,并且开始读出或写入。这些对于字符特殊文件是不可能的。系统中的所有设备要么是字符文件,要么是块文件。
5. FIFO
6. 套接字
7. 符号链接
硬件设备在 Linux 中也通常被映射为文件。比较重要的设备文件有 3 个:
1. /dev/console
代表系统控制台,错误信息和诊断信息通常会被发送到这个设备。
2. /dev/tty
如果一个进程有控制终端的话,那么 /dev/tty 允许程序直接向用户输出信息,而不管用户具体使用的是哪种类型的伪终端或硬件终端。
3. /dev/null
echo "do not want to c this" > /dev/null
cp /dev/null emptyfile
二. 系统调用和设备驱动程序
操作系统的核心部分,即内核,是一组设备驱动程序。它们是一组对系统硬件进行控制的底层接口。例如,磁带机就有一个与之对应的设备驱动程序,它知道如何启动磁带、如何对它前后回绕、如何对它进行读写等。为了向用户提供一个一致的接口,设备驱动程序封装了所有与硬件相关的特性,并且通过统一的底层函数(即系统调用)进行访问。包括:
open: 打开文件或者设备
read: 从打开的文件或者设备里读数据
write: 向文件或者设备写数据
close: 关闭文件或者设备
ioctl: 把控制信息传递给设备驱动程序
注意:没有 flush,说明没有缓冲
三. 库函数
针对输入输出操作直接使用底层系统调用的一个问题是效率低下(另一个问题是,这些系统调用不属于 ANSI C ),原因有 2:
1. 执行系统调用时,Linux 必须从用户态切换到内核态,然后再返回用户态。减少这种开销的一个办法是,在程序中尽量减少系统调用的次数,并且让每次系统调用完成尽可能多的工作(这就是标准 IO 干的事情)。
2. 硬件会限制对底层系统调用一次所能读写的数据块大小。例如,磁带机通常一次能写的数据块长度为 10K,所以,如果你试图写的数据量不是 10K 的整数倍,磁带机还是会以 10K 为单位卷绕磁带,从而在磁带上留下了空隙。
四. 底层文件访问
当一个进程开始运行时,一般会有 3 个已经打开的文件描述符:
0: 标准输入
1: 标准输出
2: 标准错误
1. write 系统调用,原型为
#include <unistd.h>
size_t write(int fildes, const void *buf, size_t nbytes);
作用是,把缓冲区 buf 的前 nbytes 个字节写入与文件描述符 fildes 关联的文件中,返回实际写入的字节数。如果返回 0,就表示未写入任何数据,如果返回-1,就表示在 write 调用中出现错误,错误代码保存在 errno 中。此外,该返回值有可能小于 nbytes。
2. read 系统调用,原型为
#include <unistd.h>
size_t read(int fildes, void *buf, size_t nbytes)
作用是,从与文件描述符 fildes 相关联的文件里读入 nbytes 个字节的数据,并存放到数据区 buf 中,返回实际读入的字节数。如果返回 0,就表示未读入任何数据,如果返回 -1,就表示 read 调用出现错误。此外,该返回值可能小于请求的字节数。
3. open 系统调用,原型为
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode);
open 建立了一条到文件或者设备的访问路径。如果调用成功,将返回一个文件描述符。该文件描述符是唯一的,不会与任何其它运行中的进程共享。如果两个程序同时打开一个文件,它们会分别得到两个不同的文件描述符。如果它们都对文件进行写操作,那么它们会各写各的(这也是 FIFO 必须考虑同步问题的原因),它们分别接着上次离开的位置继续往下写。它们的数据不会交织在一起,而是彼此相互覆盖。可以通过文件锁来防止出现冲突。
准备打开的文件或者设备的名字作为参数 path 传递给函数。oflags 参数用于指定打开文件所采取的动作,是通过必需的文件访问模式与其它可选模式相结合的方式来指定的。必需的文件访问模式包括:
O_RDONLY 只读方式打开
O_WRONLY 只写方式打开
O_RDWR 读写方式打开
可选的模式包括:
O_APPEND 把写入的数据追加在文件的末尾
O_TRUNC 把文件长度设置为 0,丢弃已有的内容
O_CREAT 如果需要,就按参数 mode 中给出的权限创建文件
O_EXCL 与 O_CREAT 一起使用,确保调用者创建出文件。使用这个可选模式可以防止两个程序同时创建同一个文件,如果文件已经存在,open 调用将失败
O_DIRECT try to minimize cache effects of the I/O to and from this file作用类似 volatile
open 调用在成功时返回一个新的文件描述符,它总是一个非负整数,在失败时返回 -1 并且设置 errno。新文件描述符总是使用未用描述符的最小值,这个特征在某些情况下非常有用。例如,如果一个程序关闭了它的标准输出,然后再次调用 open,文件描述符 1 就会被重新使用,并且标准输出将被有效地重定向到另一个文件或设备,即,以前写往标准输出的内容,将写往对应的文件或者设备。
任何一个运行中的程序能够同时打开的文件数是有限制的。这个限制通常是由 limits.h 头文件中的常量 OPEN_MAX 定义的。在 Linux 中,这个限制可以在系统运行时调整,通常一开始被设置为 256。
4. 访问权限的初始值
当使用带有 O_CREAT 标志的 open 调用来创建文件时,必须使用有 3 个参数格式的 open 调用。第 3 个参数 mode 是几个标志位按位或后得到的。包括:
S_IRUSR 读权限,文件属主
S_IWUSR 写权限
S_IXUSR 执行权限
S_IRGRP 读权限,文件属组
S_IWGRP 写权限
S_IXGRP 执行权限
S_IROTH 读权限,其它用户
S_IWOTH 写权限
S_IXOTH 执行权限
有几个因素会对文件的访问权限产生影响。首先,指定的访问权限只有在创建文件时才会使用。其次,umask 会影响到被创建文件的访问权限。open 调用里给出的 mode 值将与当时的 umask 的反值做 AND 操作。因此,open 和 creat 调用中的标志实际上是发出设置文件访问权限的请求,所请求的权限是否会被设置取决于当时 umask 的值。
5. 其它与文件管理有关的系统调用
a. lseek 系统调用
用于为文件的读写指针设置下一个读写位置。
#include <unistd.h>
#include <sys/types.h>
off_t lseek(int fildes, off_t offset, int whence);
offset 用来指定位置。whence 用于定义 offset 的相对位置,取值如下:
SEEK_SET: offset 是一个绝对位置
SEEK_CUR: offset 是相对于当前位置的一个相对位置
SEEK_END: offset 是相对于文件尾的一个相对位置
其返回值是从文件头到文件指针被设置处的字节偏移值,失败时返回 -1。
b. fstat、stat 和lstat 系统调用
返回与打开的文件描述符相关的文件的状态信息,包括文件权限、文件类型、inode、保存文件的设备、属主的 UID 号、属组的 GID 号、硬链接的个数等等。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int fstat(int fildes, struct stat *buf);
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);
fstat 以文件描述符为参数。stat 和 lstat 以文件路径为参数,并且产生相同的结果,但是当文件是一个符号链接时,lstat 返回的是该符号链接本身的信息,而 stat 返回的是该链接指向的文件的信息(即,后者看不到符号链接)。
配合使用的,还有 S_ISXXX 宏,包括 S_ISREG、S_ISDIR、S_ISCHR、S_ISBLK、S_ISFIFO、S_ISLNK、S_ISSOCK。使用方法为:
S_ISREG(buf.st_mode);
c. dup 和 dup2 系统调用
dup 系统调用提供了一种复制文件描述符的方法,使我们能够通过两个或者更多个不同的描述符来访问同一个文件。dup 系统调用复制文件描述符 fildes,返回一个新的描述符。dup2 系统调用则是通过明确指定目标描述符来把一个文件描述符复制为另外一个。
五. 标准 I/O 库
标准 I/O 库及其头文件 stdio.h 为底层 I/O 系统调用提供了一个通用的接口。这个库现在已经成为 ANSI 标准 C 的一部分,而前面的系统调用却还不是。标准 I/O 库提供了许多复杂的函数用于格式化输出和扫描输入。它还负责满足设备的缓冲需求(这是底层 I/O 所不具备的)。
在标准 I/O 库中,与底层文件描述符对应的是流,它被实现为指向结构 FILE 的指针。在启动程序时,有 3 个文件流是自动打开的。它们是 stdin、stdout 和 stderr。它们都是在 stdio.h 头文件里定义的,分别代表标准输入、标准输出和标准错误输出,与底层文件描述符 0、1 和 2 相对应。
每个文件流都和一个底层文件描述符相关联。可以把底层的输入输出操作与高层的文件流操作混合使用,但一般不建议这样做,因为数据缓冲的后果难以预料。
#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
通过调用 fileno 函数来确定文件流使用的是哪个底层文件描述符,它返回的是文件流使用的文件描述符,失败则返回-1。通过调用 fdopen 函数在一个已打开的文件描述符上创建一个新的文件流,实质上,这个函数的作用是为一个已经打开的文件描述符提供 stdio 缓冲区。
另外,注意 feof() 导致的最后一行重复出现的问题。
六. fcntl
#include <fcntl.h>
int fcntl(int fildes, int cmd);
int fcntl(int fildes, int cmd, long arg);
通过该系统调用,可以对打开的文件描述符执行各种操作,包括对它们进行复制、获取和设置文件描述符标志、管理建议性文件锁、设置文件的读写方式(阻塞/非阻塞)等。
七. mmap
creates a new mapping in the virtual address space of the calling process.。
1. 建立一段可以被两个或更多程序读写的内存,一个程序对它所做的修改,可以选择被其它程序看见
2. 用于文件处理,使某个磁盘文件的全部内容看起来就像是内存中的一个数组,特别是如果文件由记录组成,而这些记录又可以用 C 中的数据结构表示时,就可以通过访问结构数组来更新文件内容。
#include <sys/mman.h>
void mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
addr 表示内存的地址,通常建议为 NULL,采用自动分配的方式,提高可移植性,此时,mmap 的返回值不再是 void
len 为这段内存的长度
prot describes the desired memory protection of the mapping, must not conflict with the open mode of the file
PROT_READ 允许读该内存段
PROT_WRITE 允许写该内存段
PROT_EXEC 允许执行该内存段
PROC_NONE 该内存段不能被访问 使用场景?
flag 控制程序对该内存段的改变所造成的影响,包括其它进程是否可见、是否写回底层对应的文件,取值如下:
MAP_PRIVATE Create a private copy-on-write mapping 注意是 copy-on-write 映射,即除非发生写操作,否则不会 copy
Updates to the mapping are not visible to other processes mapping the same file, and are not carried through to the underlying file. It is unspecified whether changes made to the file after the mmap() call are visible in the mapped region.即,内存和文件的同步是双向的,该选项只保证内存的修改不会影响文件,也不影响 mmap() 了该文件的其它进程,但是,不保证另外一个方向,即,在 mmap() 调用之后,如果对应的底层文件发生了变化,该变化是否可见是不保证的。但是从 Ubuntu 测试的结果看来,外部文件发生的变化是立即可见的。
MAP_SHARED 把对该内存段的修改保存到磁盘中
The file may not actually be updated until msync(2) or munmap() is called. 即,update 需由 msync 或者 munmap 保证,而不保证自行 update。(不保证的意思是,可能会写,也可能不写,纯粹看心情) 另外,此时应该也是 copy-on-write 吧?
flag 参数不仅仅只有这两种,其实多了去了:
MAP_FIXED 该内存段必须位于 addr 指定的地址处,不鼓励如此使用
MAP_ANONYMOUS The mapping is not backed by any file; its contents are initialized to zero. The fd and offset arguments are ignored; however, some implementations require fd to be -1 if MAP_ANONYMOUS (or MAP_ANON) is specified, and portable applications should ensure this. The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is only supported on Linux since kernel 2.4.
fildes 即为 open 打开的文件描述符
off 为文件描述符中数据的起始偏移量
The contents of a file mapping (as opposed to an anonymous mapping; see MAP_ANONYMOUS below), are initialized using length bytes starting at offset offset in the file (or other object) referred to by the file descriptor fd. offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE).
上面这段话让我疑惑。mmap() 不会产生内存拷贝,那么,也就不应该存在 initialize 的说法。估计只能理解成,表象与 initialize 类似:申请了指针,而且给指针赋值,但实质上,赋值并不是 memcpy。
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
msync() flushes changes made to the in-core copy of a file that was mapped into memory using mmap(2) back to disk.
addr 即为 mmap 返回或者指定的地址,len 为该地址的长度
flags 的取值包括:
MS_ASYNC specifies that an update be scheduled, but the call returns immediately.
MS_SYNC asks for an update and waits for it to complete.
MS_INVALIDATE: asks to invalidate other mapping of the same file, so that they can be updated with the fresh values just written. 让其它进程获取实时信息
如果某进程对内存的修改需要被其它进程看见,不仅仅是设置 flag 为 MAP_SHARED(这仅仅是设置属性),还需要调用 msync(主动同步),同时 msync 的 flag 参数还需要带上 MS_INVALIDATE 选项,即,不仅要更新文件,还要让其它进程重新 mmap()
#include <sys/mman.h>
int munmap(void *addr, size_t len);
The munmap() system call deletes the mappings for the specified address range, and causes further references to addresses within the range to generate invalid memory references. The region is also automatically unmapped when the process is terminated. On the other hand, closing the file descriptor does not unmap the region.
The address addr must be a multiple of the page size. All pages containing a part of the indicated range are unmapped, and subsequent references to these pages will generate SIGSEGV. It is not an error if the indicated range does not contain any mapped pages. 最后这一句啥意思?
Timestamps changes for file-backed mappings
For file-backed mappings, the st_atime field for the mapped file may be updated at any time between the mmap() and the corresponding unmapping; the first reference to a mapped page will update the field if it has not been already.
The st_ctime and st_mtime field for a file mapped with PROT_WRITE and MAP_SHARED will be updated after a write to the mapped region, and before a subsequent msync(2) with the MS_SYNC or MS_ASYNC flag, if one occurs.
八. 临时文件
自行生成临时文件的唯一缺点,是必须由应用程序生成唯一的文件名。Linux 对此提供了几个系统调用。
#include <stdio.h>
char *tmpnam(char *s);
该函数返回一个不与任何已存在文件同名的有效文件。如果字符串 s 不为空,文件名也会写入 s。s 的长度至少要有 L_tmpnam(通常为 20) 个字符。tmpnam 可以被一个程序最多调用 TMP_MAX 次(至少为几千次),每次都会返回不同的文件名。
其缺点是,必须尽可能快的打开它,以减小另一个程序用同样的名字打开文件的风险。所以,可以使用 tmpfile 函数:
#include <stdio.h>
FILE *tmpfile(void);
该函数返回一个文件流指针,它指向一个唯一的临时文件,该文件以读写的方式打开,当对它的所有引用全部关闭时,该文件会被自动删除。
此外,还有另外一种生成临时文件的方法,就是使用 mktemp 和 mkstemp 函数。它们与 tmpnam 类似,不同之处在于可以为临时文件指定一个模板,模板可以对文件路径和文件名有更多的控制。
#include <stdlib.h>
char *mktemp(char *template);
int mkstemp(char *template);
mktemp 函数以给定的模板为基础,创建一个唯一的文件名。template 参数必须是一个以 6 个 X 字符结尾的字符数组。mktemp 函数用有效文件名字符的一个唯一组合来替换这些 X 字符。
mkstemp 函数类似于 tmpfile,它也是同时创建并打开一个临时文件。文件名的生成方法和 mktemp 一样,但是它的返回值是一个打开的、底层的文件描述符(所以不能用标准 IO 库)。
九. 扫描目录
十. 错误处理
许多系统调用和函数都会因为各种各样的原因而失败,它们会在失败时设置外部变量 errno 的值来指明失败的原因。程序必须在函数报告出错之后立即检查 errno 变量,因为它可能被下一个函数调用所覆盖,即使下一个函数自身并没有出错,也可能会覆盖这个变量。
有两个非常有用的函数可以用来报告出现的错误:
1. strerror 函数
#include <string.h>
char *strerror(int errnum);
该函数把错误代码映射为一个字符串,该字符串对发生的错误类型进行说明。
个人认为该函数的价值不大。首先要保存系统调用返回的错误码,然后以该错误码为参数调用该函数。但是很多系统调用的返回值是指针,此时就无法获取错误码。
2. perror 函数
#include <stdio.h>
void perror(const char *s);
该函数也把 errno 变量中报告的当前错误映射到一个字符串,并把它输出到标准错误输出流。该字符串的前面先加上字符串 s 中给出的信息,再加上一个冒号和一个空格。注意,字符串 s 不能为空。
实际使用时,可以把 s 填成系统调用的名称,后续就不用再傻兮兮的用 printf 打印 errno 了,另外,即使打印了errno,还得去查找代表啥意思,而现在 perror 全部替你搞定。