apue(二)、文件IO

137 阅读8分钟

1、文件I/O概述

  • 文件I/O 需要区别于标准I/O,文件I/O 只会用到5个函数:open、read、write、lseek、close;
  • 文件I/O 函数是不带缓冲的I/O,与标准I/O不同。不带缓冲是指每个函数都调用内核中的一个系统调用,这些不带缓冲的I/O是 POSIX.1的组成部分。

2、文件描述符

  • 对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负数,打开或创建一个新文件时,内核向进程返回一个文件描述符。Unix系统分别关联 0—标准输入、1—标准输出、2—标准错误。
  • 文件描述符的变化范围是 0 ~ OPEN_MAX-1OPEN_MAX 在 POSIX限制中指定为20,也就是说允许每个进程最多打开20个文件。但实际上每个进程可以打开超过20个文件,在 Linux3.2.0、FreeBSD8.0 等操作系统中文件描述符的变化范围几乎无限,它只受到系统配置的存储器总量、整型的字长约束。

3、open、openat

#include<fcntl.h>
int open(const char *path, int oflag, ...)    //path:文件名   oflag:文件状态
int openat(int fd, const char *path, int oflag, ...)  //fd:相对路径文件描述符   path:文件相对路径

openat函数:

  • 如果path参数指定的是绝对路径名,则fd参数会被忽略。openat函数相当于open;
  • path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取;
  • path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。这种情况下,路径名在当前工作目录中获取。openat()在操作上与open()函数类似;

oflag参数:

  • 选项在头文件 <fcntl.h> 中定义
  • 下面5个选项必选一个,且只能选一个:
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读、写打开
O_EXEC只执行打开
O_SEARCH只搜索打开(应用于目录)。目的在于目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查该目录的搜索权限(目前还不支持这个选项)
  • 可选选项:
O_APPEND每次写时都追加到文件的尾端
O_CLOEXECFD_CLOEXEC常量设置为文件描述符标志
O_CREAT若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,mode代表该新文件的权限
O_DIRECTORY如果path引用的不是目录,则出错
O_EXCL如果同时指定了O_ CREAT,而文件已经存在,则出错。这可测试一个文件是 否存在,如果不存在则创建此文件。使得测试和创建成为一个原子操作
O_NONBLOCK如果path引用的是一个FIFO、特殊文件,则此选项为文件本次打开操作和后续的I/O操作设置为非阻塞方式
O_SYNC每次write操作需要等待物理I/O完成,包括数据和属性的更新
O_TRUNC如果文件已存在,而且为只写或读写成功打开,则将长度截断为0
O_DSYNC每次write操作需要等待物理I/O完成,但只更新数据
O_RSYNC每一个以文件描述符作为参数进行的read操作都需要等待,直至所有对文件同一部分挂起的写操作都完成

openat解决的问题:

  • 同一进程中的所有线程共享相同的当前工作目录,openat使得线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录;
  • 避免了 time-of-check-to-time-of-use 问题,它指的是有两个基于文件的函数调用,第二个调用依赖于第一个调用的结果,这样就导致了不再是原子操作,两次调用过程中可能会导致结果发生变化。

3、creat

#include <fcntl.h>
int creat(const char *path, mode_t mode);

等效于只写 open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)或者读写 open(path, O_RDWR | O_CREAT | O_TRUNC, mode)

4、lseek

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence)

lseek 可以显式地为一个打开文件设置偏移量,whence 有多设置值,包括 SEEK_SET、SEEK_CUR、SEEK_END。注意只能用来获取文件偏移量,如果文件描述符指向的是管道、FIFO或者网络套接字,则 lseek 返回 -1,并将 errno 设置为 ESPIPE。某些设备文件当前偏移量可能为负值,所以在比较 lseek 返回值时应当测试它是否等于 -1,而不是是否等于 0。

//获取当前文件偏移量
off_t curpos;
curpos = lseek(fd, 0, SEEK_CUR);

//获取文件长度
off_t len;
len = lseek(fd, 0, SEEK_END);

lseek设置偏移量会导致文件空洞的产生,但是文件中的空洞并不占用磁盘上的存储区,读取文件空洞时,没有写过的字节都被读为 0。

5、read和write

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
ssize_t write(int fd, void *buf, size_t nbytes);

多种情况可能导致 read 读取到的字节数小于要求读的字节数:

  • 读普通文件,在读到要求字节数前已经到达了文件尾端;
  • 从终端设备读,每次最多读取一行;
  • 从网络读,缓冲机制导致;
  • 从管道或者FIFO读,包含字节数小于目标字节数。
//从标准输入读,输出到标准输出,不用关闭文件,进程结束时会自动关闭

#include <unistd.h>
#define BUFFSIZE 4096

int main(void) {
    int n;
    char buf[BUFFSIZE];
    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) {
        if (write(STDOUT_FILENO, buf, n) != n) {
            err_sys("write error");
        }
    }
    if (n < 0) {
        err_sys("read error");
    }
    exit(0);
}

6、文件共享

内核使用3种数据结构表示打开的文件,结构描述如下:

  • 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表。与每个文件描述符相关联的是:a.文件描述符标志(close_on_exec);b.指向一个文件表项的指针;
  • 内核为所有打开的文件维护一张文件表,每个表项包含:a.文件状态标志(读、写、添加、同步和非阻塞等);b.当前文件偏移量;c.指向文件v节点的指针;
  • 每个打开文件都有一个v节点,v节点包含了文件类型和对此文件进行各种操作函数的指针。v节点还包含了该文件的i节点,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。

image.png

以文件共享视角分析前面的函数:

  • write:每次完成 write 操作后,在文件表项中当前文件偏移量增加所写入的字节数。如果这导致文件偏移量超过了当前文件长度,则设置i节点中当前文件长度。
  • lseek:lseek改变文件表项中的文件偏移量,不进行任何 I/O 操作。

多个进程共享同一个文件,实际上每个进程都有一份打开文件的文件表项,它们共享其后的v节点、i节点。多进程读同一个文件能正常工作,但是写同一个文件则需要相关的原子操作。

7、原子操作

7.1 追加文件

就拿普通的追加数据到一个文件来说,在 open 时如果不设置 O_APPEND 选项,程序如下编写:

if (lseek(fd, OL, 2) < 0)
    err_sys("lseek error");
if (write(fd, buf, 100) != 100) 
    err_sys("write error");

先定位文件末尾,再写入,这样多进程操作同一个文件时可能会出现文件末尾被修改,而依旧照旧版本写入的问题,新数据会被覆盖掉。所以 O_APPEND 是Unix提供的一种原子操作,在每次执行写操作时都会定位到文件末尾,这样写之前就不需要调用 lseek。

7.2 pread、pwrite

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);

pread 相当于先调用 lseek后调用read,但有区别:1、在调用pread时不能中断文件定位和读操作;2、不更新当前文件偏移量。

pwrite类似于pread。

7.3 创建文件

检查文件是否存在和创建文件是两个操作,open函数的 O_EXCL 保证这两个操作作为一个原子操作执行。如果同时指定 O_CREAT和 O_EXCL,则在文件已经存在时,open 操作会失败。

8、dup 和 dup2

#include <unistd.h>
int dup(int fd);
int dup(int fd, int fd2);

dup复制当前文件描述符并返回当前可用文件描述符的最小数值;dup(fd)等效于 fnctl(fd, F_DUPFD, 0)

dup2先关闭fd2,如果fd2=fd1,则不关闭它。否则fd2 的 FD_CLOEXEC 文件描述符标志会被清除,这样fd2 在调用 exec 时是打开状态。dup2(fd, fd2)等效于close(fd2); fcntl(fd, F_DUPFD, fd2),但是后者是非原子操作,会有多线程问题。

image.png

9、sync、fsync、fdatasync

传统磁盘I/O的流程为:内核将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘,这种方式被称为延迟写。当内核需要重用缓冲区来存放其它磁盘数据时,它会将所有延迟写数据写入磁盘。

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
  • sync将所有修改过的块缓冲区排入写队列,然后返回不等待实际写磁盘操作结束;通常称为 update 的系统守护进程周期性地调用(一般每隔30s) sync函数。
  • fsync函数对指定文件描述符的文件起作用,并等待磁盘操作结束才返回。它适用于数据库这样的程序,需要确保修改过的块立刻写到磁盘上。
  • fdatasync函数类似于fsync,但是它只针对文件的数据部分,而fsync除数据外还会同步更新文件属性。

10、fcntl

#include <fcntl.h>
int fcntl (int fd, int cmd, ...)

fcntl具有以下5种功能:

  • 复制一个已有的文件描述符 (cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
  • 获取/设置文件描述符标志 (cmd = F_GETFD 或 F_SETFD)
  • 获取/设置文件状态标志 (cmd = F_GETFL 或 F_SETFL)
  • 获取/设置异步I/O所有权 (cmd = F_GETOWN 或 F_SETOWN)
  • 获取/设置记录锁 (cmd = F_GETLK、F_SETLK、F_SETLKW)

F_DUPFD 复制一个文件描述符,新的文件描述符的 FD_CLOEXEC 标志会被清除。

F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的 FD_CLOEXEC 文件描述符标志的值,返回新文件描述符。

F_GETFL 获取文件状态标志,文件状态标志在 open函数中声明过。注意前5个访问标志并不各占1位,一个文件的访问方式只能取这5个值之一。因此首先必须使用屏蔽字 O_ACCMODE 取得访问方式,然后和这5个值相比较。这5个值分别是 O_RDONLY、O_WRONLY、O_RDWR、O_EXEC、O_SEARCH。

F_SETFL 设置文件状态标志,可以更改的值包括 O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC、O_ASYNC。

如果要新增文件状态标志时,必须先根据文件描述符利用 fcntl的 F_GETFL命令获取到旧标志然后或上新标志(val |= flags),最后通过 F_SETFL设置标志;如果要去除文件标志时,同理通过 F_GETFL命令获取旧标志,然后与上旧标志的反值(val & = ~flags)。

//测试fcntl函数
#include <fcntl.h>
#include <stdio.h>
#include "../../include/err.h"

int main(int argc, char *argv[]) {
    int val;
    if (argc != 2) 
        err_quit("err input!");
    if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) {
        err_sys("fcntl error for fd %d", atoi(argv[1]));
    }
    switch (val & O_ACCMODE) {
        case O_RDONLY:
            printf("read only");
            break;
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;
        default:
            err_dump("unknown access mode");
    }
    if (val & O_APPEND)
        printf(", append");
    putchar('\n');
    exit(0);
}

注意在修改文件描述符标志或者文件状态时必须谨慎,先要获得现在的标志值,然后按照期望修改它,最后设置新标志值。不能仅仅执行 F_SETFD 或者 F_SETFL 命令,这样会关闭以前设置的标志值。

#include <fcntl.h>

void set_fl(int fd, int flags) {
    int val;
    if ((val = fcntl(fd, F_GETFL, 0)) < 0)
        err_sys("fcntl F_GETFL error");
    val |= flags;  /* turn flags on */
    val &= ~flags; /* turn flags off */
    if (fcntl(fd, F_SETFL, val) < 0)
        err_sys("fcntl F_SETFL error");
}