一、文件描述符
? 所有执行I/O操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。针对每个进程,文件描述符都自成一套。
二、通用I/O的系统调用
? 通用I/O 可以使用四个同样的系统调用open、read、write、close对所有类型的文件执行I/O操作。要实现通用I/O,就必须确保每一个文件系统和设备驱动程序都实现了相同的I/O系统调用集。
2.1 open
? open()系统调用打开pathname指定的文件。如果指定的文件不存在,flags 中有O_CREAT标记,open()将创建该文件。调用成功返回值是一个文件描述符,若发生错误,则返回-1,并将errno置为相应的错误标志。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:要打开的文件路径(相对路径或绝对路径),如果是一个符号链接,会对其进行解引用。
flags:位掩码,用于指定文件的访问模式,当flags中包含O_CREAT标志时,需要设置mode参数。
FLAG | 用途 | 说明 |
---|
O_RDONLY | 以只读方式打开 | | O_WRONLY | 以只写方式打开 | | O_RDWR | 以读写方式打开 | | O_CLOEXEC | 设置 close-on-exec 标志(自 Linux 2.6.23 版本开始) | 在多线程程序中执行 fcntl() 的 F_GETFD和 F_SETFD 操作有可能导致竞争状态,而使用 O_CLOEXEC 标志则能够避免这一点。可能引发竞争的场景是:线程某甲打开一文件描述符,尝试为该描述符标记 close-on-exec 标志,于此同时,线程某乙执行 fork()调用,然后调用 exec()执行任意一个程序。(假设在某甲打开文件描述符和调用fcntl()设置 close-on-exec 标志之间,某乙成功地执行了 fork()和 exec()操作。)此类竞争可能会在无意间将打开的文件描述符泄露给不安全的程序。 | O_CREAT | 若文件不存在则创建之 | 如果文件不存在,将创建一个新的空文件。即使文件以只读方式打开,此标志依然有效。如果在 open()调用中指定 O_CREAT 标志,那么还需要提供 mode 参数,否则,会将新文件的权限设置为栈中的某个随机值。 | O_DIRECT | 无缓冲的输入/输出 | 必须定义_GNU_SOURCE 功能测试宏 | O_DIRECTORY | 如果 pathname 不是目录,则失败 | 如果 pathname 参数并非目录,将返回错误(错误号 errno 为 ENOTDIR)。这一标志是专为实现 opendir()函数(18.8 节)而设计的扩展标志。为使 O_DIRECTORY 标志的常量定义在<fcntl.h>中有效,必须定义_GNU_SOURCE 功能测试宏。 | O_EXCL | 结合 O_CREAT 参数使用,专门用于创建文件 | 此标志与 O_CREAT 标志结合使用表明如果文件已经存在,则不会打开文件,且 open()调用失败,并返回错误,错误号 errno 为 EEXIST。换言之,此标志确保了调用者(open( )的调用进程)就是创建文件的进程。检查文件存在与否和创建文件这两步属于同一原子操作。 | O_LARGEFILE | 在 32 位系统中使用此标志打开大文件 | 支持以大文件方式打开文件。在 32 位操作系统中使用此标志,以支持大文件操作。 | O_NOATIME | 调用 read()时,不修改文件最近访问时间(自 Linux 2.6.8版本开始) | 要使用该标志,要么调用进程的有效用户 ID 必须与文件的拥有者相匹配,要么进程需要拥有特权(CAP_FOWNER)。否则,open()调用失败,并返回错误,错误号 errno 为 EPERM。。(事实上,对于非特权进程,当以 O_NOATIME 标志打开文件时,与文件用户 ID 必须匹配的是进程的文件系统用户 ID,而非进程的有效用户 ID。)此标志是 Linux 特有的非标准扩展。要从<fcntl.h>中启用此标志,必须定义_GNU_SOURCE 功能测试宏。 | O_NOCTTY | 不要让 pathname(所指向的终端设备)成为控制终端 | 如果正在打开的文件属于终端设备,O_NOCTTY 标志防止其成为控制终端。如果正在打开的文件不是终端设备,则此标志无效。 | O_NOFOLLOW | 对符号链接不予解引用 | 通常,如果 pathname 参数是符号链接,open()函数将对 pathname 参数进行解引用。一旦在 open()函数中指定了 O_NOFOLLOW 标志,且 pathname 参数属于符号链接,则 open()函数将返回失败(错误号 errno 为 ELOOP)。此标志在特权程序中极为有用,能够确保 open()函数不对符号链接进行解引用。为使 O_NOFOLLOW 标志在<fcntl.h>中有效,必须定义_GNU_SOURCE功能测试宏。 | O_TRUNC | 截断已有文件,使其长度为零 | 如果文件已经存在且为普通文件,那么将清空文件内容,将其长度置 0。在 Linux 下使用此标志,无论以读、写方式打开文件,都可清空文件内容(在这两种情况下,都必须拥有对文件的写权限)。 | O_APPEND | 总在文件尾部追加数据 | | O_ASYNC | 当 I/O 操作可行时,产生信号(signal)通知进程 | 当对于 open()调用所返回的文件描述符可以实施 I/O 操作时,系统会产生一个信号通知进程。这一特性,也被称为信号驱动 I/O,仅对特定类型的文件有效,诸如终端、FIFOS 及 socket。在 Linux 中,调用 open()时指定 O_ASYNC 标志没有任何实质效果。要启用信号驱动 I/O 特性,必须调用 fcntl()的 F_SETFL 操作来设置 O_ASYNC 标志。 | O_DSYNC | 提供同步的 I/O 数据完整性(自 Linux 2.6.33 版本开始) | | O_NONBLOCK | 以非阻塞方式打开 | | O_SYNC | 以同步方式写入文件 | |
mode:此参数用于指定新建文件的访问权限。
S_IRUSR | 允许文件所属者读文件 |
---|
S_IWUSR | 允许文件所属者写文件 | S_IXUSR | 允许文件所属者执行文件 | S_IRWXU | 允许文件所属者读、写、执行文件 | S_IRGRP | 允许同组用户读文件 | S_IWGRP | 允许同组用户写文件 | S_IXGRP | 允许同组用户执行文件 | S_IRWXG | 允许同组用户读、写、执行文件 | S_IROTH | 允许其他用户读文件 | S_IWOTH | 允许其他用户写文件 | S_IXOTH | 允许其他用户执行文件 | S_IRWXO | 允许其他用户读、写、执行文件 | S_ISUID | set-user-ID(特殊权限) | S_ISGID | set-group-ID(特殊权限) | S_ISVTX | sticky(特殊权限) |
2.2 read
read()系统调用从文件描述符 fd 所指代的打开文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
count 参数指定最多能读取的字节数。(size_t 数据类型属于无符号整数类型。)buffer 参数提供用来存放输入数据的内存缓冲区地址。缓冲区至少应有 count 个字节。如果 read()调用成功,将返回实际读取的字节数,如果遇到文件结束(EOF)则返回 0,如果出现错误则返回-1。ssize_t 数据类型属于有符号的整数类型,用来存放(读取的)字节数或-1(表示错误)。
2.3 write
write()系统调用将数据写入一个已打开的文件中。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write()调用的参数含义与 read()调用相类似。buffer 参数为要写入文件中数据的内存地址,count参数为欲从 buffer 写入文件的数据字节数,fd 参数为一文件描述符,指代数据要写入的文件。如果 write()调用成功,将返回实际写入文件的字节数,该返回值可能小于 count 参数值。这被称为“部分写”。对磁盘文件来说,造成“部分写”的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。对磁盘文件执行 I/O 操作时,write()调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快 write()系统调用,内核会缓存磁盘的 I/O 操作。
2.4 close
close()系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭其已打开的所有文件描述符。
#include <unistd.h>
int close(int fd);
文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。在编写需要长期运行并处理大量文件的程序时,比如 shell 或者网络服务器软件,需要特别加以关注。像其他所有系统调用一样,应对 close()的调用进行错误检查,如下所示:
if(close(fd)==-1)
errExit("close");
上述代码能够捕获的错误有:企图关闭一个未打开的文件描述符,或者两次关闭同一文件描述符,也能捕获特定文件系统在关闭操作中诊断出的错误条件。针对特定文件系统的错误,NFS(网络文件系统)就是一例。如果 NFS 出现提交失败,这意味着数据没有抵达远程磁盘,随之将这一错误作为 close()调用失败的原因传递给应用系统。
2.5 lseek
对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个 read()或 write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。文件打开时,会将文件偏移量设置为指向文件开始,以后每次 read()或 write()调用将自动对其进行调整,以指向已读或已写数据后的下一字节。因此,连续的 read()和 write()调用将按顺序递进,对文件进行操作。针对文件描述符 fd 参数所指代的已打开文件,lseek()系统调用依照 offset 和 whence 参数值调整该文件的偏移量。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
offset 参数指定了一个以字节为单位的数值,whence 参数则表明应参照哪个基点来解释 offset 参数,应为下列其中之一:
SEEK_SET 将文件偏移量设置为从文件头部起始点开始的 offset 个字节。
SEEK_CUR 相对于当前文件偏移量,将文件偏移量调整 offset 个字节1。
SEEK_END 将文件偏移量设置为起始于文件尾部的 offset 个字节。也就是说,offset 参数应该从文件最后一个字节之后的下一个字节算起。 lseek()调用成功会返回新的文件偏移量。
lseek()调用只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起对任何物理设备的访问。lseek()并不适用于所有类型的文件。不允许将 lseek()应用于管道、FIFO、socket 或者终端。一旦如此,调用将会失败,并将 errno 置为 ESPIPE。另一方面,只要合情合理,也可以将 lseek()应用于设备。例如,在磁盘或者磁带上查找一处具体位置。
示例:
lseek(fd,0,SEEK_SET);
lseek(fd,0,SEEK_END);
lseek(fd,-1,SEEK_END);
lseek(fd,-10,SEEK_CUR);
lseek(fd,10000,SEEK_END);
文件空洞
? 如果程序的文件偏移量已然跨越了文件结尾,然后再执行 I/O 操作,将会发生什么情况?read()调用将返回 0,表示文件结尾。有点令人惊讶的是,write()函数可以在文件结尾后的任意位置写入数据。从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞中是存在字节的,读取空洞将返回以 0(空字节)填充的缓冲区。空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入.
2.6 ioctl
ioctl()系统调用又为执行文件和设备操作提供了一种多用途机制。
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
fd 参数为某个设备或文件已打开的文件描述符,request 参数指定了将在 fd 上执行的控制操作。具体设备的头文件定义了可传递给 request 参数的常量。
|