UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek以及close,这里所涉及到的函数经常被称为不带缓冲的I/O1。只要涉及在多个进程之间共享资源,原子操作的概念就变得非常重要,我们将通过I/O和open函数的参数来讨论此概念,dup、fcntl、sync、fsync和ioctl函数提供了这方面的操作。
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负数。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联,分别为宏STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,这些宏定义在
文件描述符的变化范围是0~OPEN_MAX-1
#include
#include
#include
// return file description or -1 if an error occurred.
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
int openat(int dirfd, const char* pathname, int flags);
int openat(int dirfd, const char* pathname, int flags, mode_t mode);
pathname参数是要打开或创建文件的名字
flags参数可用来说明此函数的多个选项,用下列一个或多个常量进行或运算构成
flag | 定义 |
---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
O_EXEC | 只执行打开 |
O_SEARCH | 只搜索打开 |
O_APPEND | 写文件尾 |
O_CLOSEEX | 把FD_CLOEXEC设置为文件描述符标志 |
O_CREAT | 若文件不存在则创建 |
O_DIRECTORY | 若pathname引用的不是目录就出错 |
O_EXCL | 若同时指定了O_CREAT,则文件存在就报错 |
O_NOCTTY | 若pathname引用的是终端,则不将该设备分配为此进程的终端控制 |
O_NOFOLLOW | 若pathname引用的是符号链接,则报错 |
O_NONBLOCK | 若pathname引用的是FIFO、块特殊文件或字符特殊文件,则设置打开操作和后续I/O操作为非阻塞 |
O_SYNC | 使每次write等待物理I/O操作完成 |
O_TRUNC | 若文件存在且可写,则将其长度截为0 |
O_TTY_INIT | 若打开一个还未打开的终端设备,设置非标准termios参数值 |
O_DSYNC | 使每次write等待物理I/O完成,若写操作不影响读取刚写入的数据,则不等待文件属性被更新 |
O_RSYNC | 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成 |
fd参数区分open和openat函数,有三种可能性:
[1] pathname参数指定的是绝对路径,在这种情况下,fd参数被忽略,openat相当于open
[2] pathname参数指定的是相对路径,fd参数指出了相对路径名在文件系统中的开始地址
[3] pathname参数指定的是相对路径,fd参数给定特殊值AT_FDCWD,在这种情况下,路径名在当前工作目录中获取,openat在操作上类似open
openat函数希望解决两个问题:第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录;第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误2
#include
#include
#include
// return file description or -1 if an error occurred.
int creat(const char* pathname, mode_t mode);
// 此函数等效于
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat的缺点是只能以只写的方式打开所创建的文件。
#include
// return 0 or -1 if an erro occurred.
int close(int fd);
关闭一个文件还会释放该进程加在该文件上的所有记录锁,当一个进程终止时,内核自动关闭它所有的打开文件。
每个打开的文件都有一个与其相关联的当前文件偏移量(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并时偏移量增加所读的字节数。按系统默认情况,当打开一个文件时,除非指定了O_APPEND选项,否则该偏移量被设置为0。
#include
// return new file offset or -1 if an error occurred.
off_t lseek(int fd, off_t offset, int whence);
对参数offset的解释与参数whence有关:
[1] 若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节;
[2] 若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可正可负;
[3] 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
例程:测试涉及的文件是否可以设置偏移量
#include
#include
int main(void)
{
if (lseek(..., 0, SEEK_CUR) == -1)
printf("cann't seek\n");
else
printf("seek OK\n");
exit(0); // return 0;
}
通常,文件的当前偏移量是一个非负整数,但某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值,因此在比较lseek的返回值时应当谨慎,不要测试小于0,而要测试是否等于-1.
lseek操作仅将当前的文件偏移量记录在内核中,不引起任何I/O操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中形成一个空洞,这一点是允许的,位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。
例程:创建一个具有空洞的文件
#include
#include
#include
int main(void)
{
int fd = creat("file.hole", FILE_MODE);
if (fd < 0)
{
printf("creat error");
exit(0);
}
char buf1[] = "abcdefghij";
if (write(fd, buf1, 10) != 10)
{
printf("buf1 write error");
exit(0);
}
// now, offset is 10.
if (lseek(fd, 50, SEEK_SET) == -1)
{
printf("lseek error");
exit(0);
}
// now, offset is 60.
char buf2[] = "ABCDEFGHIJ";
if (write(fd, buf2, 10) != 10)
{
printf("buf2 write error");
exit(0);
}
exit(0);
}
#include
// the number of bytes read is return, if EOF return 0 or -1 if an error occurred.
size_t read(int fd, void* buf, size_t count);
有多种情况可使实际读到的字节数少于要求的字节数:
[1] 读普通文件时,在读到要求字节数之前就达到了文件尾。
[2] 从终端设备读时,通常时一次最多读一行。
[3] 从网络读时,网络中的缓冲机制可能造成返回值小于要求读的字节数。
[4] 从管道或FIFO读时,若管道包含的字节数少于所需数量。
[5] 从某些面向记录的设备(如磁盘)读时,一次最多返回一个记录。
[6] 当一个信号造成中断,而已经读了部分数据量时。
#include
// the number of bytes write is return or -1 if an error occurred.
size_t write(int fd, const void* buf, size_t count);
返回值通常与参数count的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满或超过了一个给定进程的文件长度限制3。
UNIX支持在不同进程间共享打开文件。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响:
[1] 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的有:文件描述符标志close_on_exec,指向一个文件表项的指针。
[2]内核为所有打开文件维持一张文件表。每个文件表项包含:文件状态标志(包括读、写、添写、同步和非阻塞等),当前文件偏移量,指向该文件v节点表项的指针。
[3] 每个打开文件或设备都有一个v-node结构。v-node包含了文件类型和对此文件进行的各种操作函数的指针。对于大多数文件,v-node还包含了该文件的i-node4。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。
一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,要么执行完所有步骤,要么一步也不执行。
#include
// the number of bytes read is return, if EOF return 0 or -1 if an error occurred.
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
// the number of bytes write is return or -1 if an error occurred.
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
调用pread相当于先调用lseek后调用read,调用pwrite与pread类似。
#include
// return new file description or -1 if an error occurred.
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE
#include
#include
int dup3(int oldfd, int newfd, int flags);
由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。
对于dup2可以用newfd参数指定新描述符,如果newfd已经打开,则先关闭newfd,如果oldfd等于newfd,则dup2返回newfd,而不关闭它。否则,newfd的FD_CLOEXEC文件描述符标志就被清除,这样newfd在进程调用exec时是打开状态。
每个文件描述符都有它自己的一套文件描述符标志,新描述符的执行时关闭(close-on-exec)标志总是由dup函数清除。
复制一个描述符的另一种方法是使用fcntl函数。
// The following two functions are the same.
dup(old_fd);
fcntl(old_fd, F_DUPFD, 0);
// The following two functions are the same.
dup2(fd, fd2);
close(new_fd); fcntl(old_fd, F_DUPFD, new_fd);
dup2是一个原子操作,而close和fcntl包含两个函数调用。
dup2和fcntl有一些不同的errno。
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。
通常,当内核需要重用缓冲区来存放其它磁盘块数据时,它会把所有延迟写数据块写入磁盘,为了保证磁盘上实际文件系统与缓冲区中内容一致,UNIX系统提供了以下几个函数。
#include
void sync(void);
// return 0 if success or -1 if an error occurred.
int fsync(int fd);
int fdatasync(int fd);
sync只是将所有修改过的块缓冲区排入写队列,然后就返回,不等待实际写磁盘操作结束。通常,称为update的系统守护进程周期性的调用(一般是30秒)sync函数,这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1)也调用sync函数。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync函数可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatasync函数类似于fsync函数,但它只影响文件的数据部分。而除数据外,fsync函数还会更新文件的属性。
fcntl函数可以改变已经打开的文件的属性。
#include
#include
int fcntl(int fd, int cmd, ... /* arg */);
根据cmd参数的值,fcntl函数有以下5种功能:
// 1. 复制一个已有的文件描述符
fcntl(fd, F_DUPFD, ...);
fcntl(fd, F_DUPFD_CLOEXEC, ...);
// 2. 获取/设置文件描述符标志
fcntl(fd, F_GETFD, ...);
fcntl(fd, F_SETFD, ...);
// 3. 获取/设置文件状态标志
fcntl(fd, F_GETFL, ...);
fcntl(fd, F_SETFL, ...);
// 4. 获取/设置一部I/O所有权
fcntl(fd, F_GETOWN, ...);
fcntl(fd, F_SETOWN, ...);
// 5. 获取/设置记录锁
fcntl(fd, F_GETLK, ...);
fcntl(fd, F_SETLK, ...);
fcntl(fd, F_SETLKW, ...);
cmd | 定义 |
---|---|
F_DUPFD | 复制文件描述符,新描述符与fd共享同一文件表项,但新米哦束缚有自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除,这表示该描述符在exec时仍保持有效 |
F_DUPFD_CLOEXEC | 复制文件描述符,设置新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符 |
F_GETFD | 对应于fd的文件描述符标志作为函数值返回,当前只定义了一个文件描述符标志FD_CLOEXEC |
F_SETFD | 对于fd设置文件描述符标志,新标志按第三个参数设置 |
F_GETFL | 对应于fd的文件状态标志作为函数值返回 |
F_SETFL | 将文件状态标志设置为第三个参数的值,可用O_APPEND O_NONBLOCK O_SYNC O_DSYNC O_RSYNC O_FSYNC O_ASYNC |
F_GETOWN | 获取当前接受SIGIO和SIGURG信号的进程ID或进程组ID |
F_SETOWN | 设置接受SIGIO和SIGURG信号的进程ID和进程组ID,正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID |
F_GETLK | 判断由arg(struct flock结构)所描述的锁是否会被另一把锁排斥,如果存在,则现有锁的信息将重写arg指向的信息,如果不存在,则除了将l_type设置为F_UNLCK之外,arg所指向结构中的其它信息保持不变 |
F_SETLK | 设置由arg(struct flock)所描述的锁,如果我们试图获得一把读锁(l_type为F_RDLCK)或写锁(l_type为F_WRLCK),而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回,此时errno设置为EACCES或EAGAIN |
F_SETLKW | 此命令也用来清除由arg指定的锁(ltype为F_UNLCK),这个命令是F_SETLK的阻塞版本 |
ioctl函数一直是I/O操作的杂物箱,不能用以上函数表示的I/O操作通常都能用ioctl函数表示,终端I/O是使用ioctl最多的地方。
#include
// return -1 if an error occurred or other value if success.
int ioctl(int fd, unsigned long request, ...);
较新的系统的都会提供名为/def/fd的目录,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。
// 大多数系统会忽略mode参数,另外一些系统要求mode必须是所引用的文件
// 初始打开时所使用的打开模式的一个子集
fd = open("/dev/fd/0", mode);
// 上面的调用方式等效于
fd = dup(0);
// 如果描述符0之前被打开为制度,则只能对fd进行读操作
// 下面的调用是成功的,仍然不能对fd进行写操作
fd = open("/dev/fd/0", O_RDWR);
术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。 ↩︎
TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。由于两个操作不是原子操作,在两个函数调用之间文件可能改变了,使得最终的结果是错误的。 ↩︎
在
i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。 ↩︎