linux系统编程-day06-文件IO(1)

  • Linux遵循一切皆文件的理念,任何你能读写的东西都可以用文件描述符来访问。

内核为每个进程维护一个打开文件的列表,该表被称为文件表(file table)。该表由一些叫做文件描述符(file descriptors)(常缩写为fds)的非负整数进行索引。
用户空间和内核空间都把文件描述符作为每个进程的唯一cookies。

  • 每个进程按照惯例会至少有三个打开的文件描述符:0:标准输入(stdin),1:标准输出(stdout),2:标准错误(stderr)。C标准库提供了预处理器宏:STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO宏,以取代对以上整数的直接引用。
  • 文件描述符不仅仅用于普通文件的访问,也用于访问设备文件、管道、目录以及快速用户空间锁、FIFOs和套接字。

打开文件

最基本的访问文件的方法是read( )和write( )系统调用,在一个文件能被访问之前,必须通过open( )或者creat( )系统调用打开它,一旦使用完毕,就应该用close( )系统调用来关闭文件。

  • open( )系统调用
#include 
#include 
#include 

int open(const char *name, int flags);
int open(const char *name, int flags, mode_t mode);

open( )系统调用将路径名name给出的文件与一个成功返回的文件描述符想关联,文件位置指针被设定为零,而文件则根据flags给出的标志位打开。

  • flags参数必须是以下之一:O_RDONLY(只读), O_WRONLY(只写), O_RDWR(可读可写)
  • flags参数可以和以下一个或多个值进行按位或运算, 用以修改打开文件请求的行为:
  1. O_APPEND: 文件以追加模式打开。在每次写操作之前,文件位置指针将被置于文件末尾。
  2. O_ASYNC: 当指定文件可写或者可读时产生一个信号(默认是SIGIO),这个标志仅用于终端和套接字,不能用于普通文件。
  3. O_CREAT: 当name指定的文件不存在时,将由内核来创建。
  4. O_DIRECT: 用于直接IO。
  5. O_DIRECTORY: 如果name不是一个目录,open( )调用将会失败。这个标志用于在opendir( )内部使用。
  6. O_EXCL: 和O_CREAT一起给出的时候,如果由name给定的文件已经存在,则open( )调用失败。用来防止文件创建时出现竞争。
  7. O_LARGEFILE: 给定文件打开时将使用64位偏移量,这样大于2G的文件也能被打开。
  8. O_NOCTTY: 如果name指向一个终端设备(/dev/tty),它将不会成为这个进程的控制终端,即时该进程目前没有控制终端。
  9. O_NOFOLLOW: 如果name是一个符号链接,open( )调用会失败。
  10. O_NONBLOCK: 如果可以,文件将在非阻塞模式下打开。open( )调用不会,任何其它操作都不会使该进程
  11. O_SYNC: 打开文件用于同步IO。
  12. O_TRUNC: 如果文件存在,且为普通文件,并允许写,将文件的长度截断为0。
  • 新文件所有者
    文件所有者的用户ID就是创建该文件的进程的有效用户id。
    创建文件的进程的组id赋予该文件的用户组。

  • 新文件权限
    除非创建了新文件,否则mode参数会被忽略。而当O_CREAT给出时则必须提供mode参数。

  • 如果在使用O_CREAT时忘记了提供mode参数,结果是未定义的,会很糟糕!

当文件创建时,mode参数提供新建文件的权限。mode参数是常见的Unix权限位集合,像八进制数0644(所有者可以读写,其他人只能读)。例如:

int fd;
fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR | S_IWGRP | S_IRGRP | S_IROTH);
if (fd == -1)
    /* error */
  • creat( ) 函数
    O_WRONLY | O_CREAT | O_TRUNC组合经常被使用,以至于专门有个系统调用来实现:
#include 
#include 
#include 
int creat (const char *name, mode_t mode);

对creat的调用:

int fd;
fd = creat(file, 0644);
if (fd == -1)
    /* error */

等价于:

int fd;
fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
   /* error */
  • 返回值和错误码
    open( )和creat( )系统调用成功时都返回一个fd,错误时都返回-1,并且将errno设置为一个合适的错误值。

用read( )读取文件

最基本、最常见的读取文件的机制是使用read( )系统调用:

#include 
ssize_t read(int fd, void *buf, size_t len);

该系统调用从fd指向的文件的当前偏移量至多读len个字节到buf中。成功时,将返回写入buf中的字节数。出错时,返回-1,并设置errno。

  • 一次读后,fd所指文件位置指针将会向前移动,移动的长度由之前读取的字节数决定。(如果无法在该文件(比如一个字符设备文件)中确定文件位置,读操作总是从“当前”位置开始)

  • 返回值
    返回值可能有以下结果:

  1. 等于len。则与预期一致
  2. 大于0但是小于len。合法,读取的字节存入buf中。
  3. 返回0。标志着EOF,没有可以读入的数据
  4. 返回-1,errno被设置为EINTR。表示在读入字节之前收到了一个信号,可以重新进行调用。
  5. 返回-1, errno被设置为EAGAIN。这表示读取会因为没有可用的数据而阻碍,而读请求应该在之后重开。这只在非阻塞模式下发生。
  6. 返回-1, errno被设置不同于EINTR或EAGAIN的值。这表示某种更严重的错误。
  • 读入所有的字节
    读入所有len个字节(至少读到EOF)的一个例子:
ssize_t ret;
while (len != 0 && (ret = read(fd, buf, len) != 0)) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror("read");
    break;
  }
  len -= ret;
  buf += ret;
}
  • 非阻塞读
    有时候,程序员不希望当没有可读数据时让read( )调用阻塞。相反,他们倾向于在没有可读数据时,让调用立即返回。这种情况被称为非阻塞I/O
  • 如果给定的fd在非阻塞模式下打开(open( )中给定O_NONBLOCK)并且没有可读数据,
    read( )调用会返回-1,且设置errno为EAGAIN而不是阻塞掉。
  • 在进行非阻塞I/O时,必须检查EAGAIN,否则将可能因数据缺失而导致严重的错误。
char buf[BUFSIZE];
ssize_t nr;
start:
  nr = read(fd, buf, BUFSIZE);
  if (nr == -1) {
    if (errno == EINTR)
      goto start; 
    if (errno == EAGAIN)
      /* resubmit later */
    else
      /* error */
  }
  • read( )大小限制
    在32系统上,size_t是unsigned int类型,ssize_t是有符号的size_t类型。
    size_t的最大值为SIZE_MAX;ssize_t的最大值为SSIZE_MAX。如果len比SSIZE_MAX大,read( )调用的结果是未定义的。
  • 在大部分linux系统上,SSIZE_MAX是LONG_MAX,在32位系统上即0x7fffffff。
  • 可增加如下代码:
if (len > SSIZE_MAX)
  len = SSIZE_MAX;
  • 一个len为0的read( )调用的结果是立即返回且返回值为0.

用write( )来写

#include 
ssize_t write(int fd, const void *buf, size_t count);

一个write( )调用从由文件描述符fd引用文件的当前位置开始,将buf中至多count个字节写入文件。

  • 对于普通文件来说,除非发生一个错误,否则write( )将保证写入所有的请求。
  • 对于其他类型的文件--例如套接字--得有个循环来保证你真的写入了所有请求的字节。使用循环的另一个好处是,第二个write( )调用可能会返回一个错误值用来说明第一次调用为什么进行了一次部分写(尽管这种情况并不常见)。以下是一个实例代码:
ssize_t ret, nr;
while (len != 0 && (ret = write(fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror("write");
    break;
  }
  len -= ret;
  buf += ret;
}
  • 追加模式
    打开文件时,open( )通过指定O_APPEND参数,则在追加模式下打开。写操作就总是从当前文件末尾开始。

  • 追加写模式可使多个进程访问同个文件不出错。

  • ** 非阻塞写**
    当fd在非阻塞模式下打开时(通过设置O_NONBLOCK参数),并且发起的写操作会正常阻塞时,write( )系统调用返回-1,并设置errno值为EAGAIN。请求应该在稍后重新发起。通常普通文件不会出现这种情况。

  • write( )大小限制
    如果count比SSIZE_MAX还大,write( )调用的结果是未定义的。count值为0的write( )调用将会立即返回且返回值为0。

  • write( )的行为
    write是先写入page cache后就立刻返回了,然后由内核的后台线程来writeback所有的"脏"缓冲区,将它们排好序,并写入到磁盘上。

同步IO

由于一般都使用page cache & writeback的机制,然而又有应用想要控制数据写入磁盘的时间,于是Linux提供了同步机制,用性能来换取同步操作。

  1. fsync( )
#include 
int fsync(fd);

调用fsync( )可保证fd对应文件的脏数据回写到磁盘上。

  1. fdatasync( ):
#include 
int fdatasync(fd);

和fsync( )的区别在于,它仅仅写入数据,不保证元数据同步到磁盘上。

  • 注:这两个调用都不保证任何已经更新的包含该文件的目录项同步到磁盘,如果要保证目录项也同步,必须对目录本身也调用fsync( )进行同步
  1. sync( ):
#include 
void sync(void);

sync( )用来对磁盘上的所有缓冲区进行同步!

  • sync( )没有参数,也没有返回值。它总是成功返回,并确保所有的缓冲区--包括数据和元数据--都能写入磁盘。
  • sync( )并非一直等待到所有缓冲区都写到磁盘才返回,只需要调用它来启动将整个缓冲区写入磁盘的过程即可。
  1. O_SYNC标志
    O_SYNC标志在open( )中使用,使所有在文件上的I/O操作同步。例如:
int fd;
fd = open(file, O_WRONLY | O_SYNC);
if (fd == -1){
  perror("open");
  return -1;
}
  • O_SYNC看起来像是在每个write( )操作后都隐式地执行fsync( ).
  • O_SYNC会使总耗时增加一到两个数量级
  • 如果一般要确保数据写入磁盘的应用可以使用fsync( )或者fdatasync( )。相比O_SYNC来说,开销更小一点。
  1. O_DSYNC和O_RSYNC
  • O_DSYNC:只有普通数据被同步(与fdatasync( )类似)
  • O_RSYNC: 保证读请求同步。该标志只能和O_SYNC或O_DSYNC一起使用。O_RSYNC保证每次读操作后,元数据也立刻更新。

直接IO

由于Linux内核实现了一个复杂的缓存、缓冲以及设备和应用之间的I/O管理的层次结构。通过O_DIRECT标志使内核最小化I/O管理的影响。

  • 使用O_DIRECT标志时,I/O操作将忽略page cache机制,直接对用户空间缓冲区和设备进行初始化,所有的I/O将是同步的,操作在完成前不会返回。
  • 当使用直接I/O时,请求长度,缓冲区对齐,和文件偏移必须是设别扇区大小(通常是512字节)的整数倍。

关闭文件

程序完成对某个文件的操作后,可以使用close( )系统调用将文件描述符和对应的文件解除关联。

#include 
int close(int fd);

close( )调用解除了已打开的文件描述符的关联,并分离进程和文件的关联。

  • 关闭文件和文件被写入磁盘没有关系
  • 关闭文件也有些副作用:fd关闭后,在内核中表示该文件的数据结构就被释放了。当它释放时,与文件关联的inode的内存拷贝被清除.

你可能感兴趣的:(linux系统编程-day06-文件IO(1))