Unix I/O 概述

一个Unix文件就是一个m个字节的序列,所有的I/O设备(如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作相应文件的读和写来执行。这种将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式执行:

  • 打开文件: 一个应用程序通过要求内核打开相应的文件,来宣告它想访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。(内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符,用户程序只能通过文件描述符引用文件) 为了终端使用方便,shell启动一个程序时,该程序会自动继承3个打开的文件,描述符分别为0、1、2,分别为标准输入、标准输出、标准错误。所有这些都默认连接到终端,因此如果一个程序只读文件描述符0,只写描述符1、2,那么它不必打开任何其他文件就可以完成I/O操作。
  • 改变当前文件的位置: 对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式的设置文件的当前位置为k。
  • 读写文件: 读操作就是从文件拷贝n个字节到存储器(n>0,存储器一般指程序中的字符数组),从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读写会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。(在文件结尾处并没有明确的EOF符号)。类似的,写操作就是从存储器拷贝n个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件: 当应用完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因而终止时,内核都会关闭所有打开的文件并释放它们的存储器资源。

一个FILE指针指向一个结构,该结构包含了文件描述符和其他一些信息。宏fileno(fp)定义在<stdio.h>中,它返回文件描述符。更多相关信息:C I/O

打开和关闭文件(删除)

进程通过调用open来打开一个已存在文件或创建一个新文件。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 成功返回文件描述符,若失败,则返回-1
int open(char *filename, int flags, mode_t mode);
  1. openfilename转换为一个文件描述符,并且返回描述符。返回的描述符总是在进程中当前没有打开的最小描述符。

  2. flags指明了进程打算如何访问这个文件,flags的参数可以是一个或更多位掩码的或,为此提供一些额外的提示。

    • O_RDONLY: 只读
    • O_WRONLY: 只写
    • O_RDWR: 可读可写
    • O_CREAT: 如果文件不存在,就创建它的一个截断的(truncated)(空)文件
    • O_TRUNC: 如果文件已经存在,就截断它
    • O_APPEND: 在每次写之前,设置文件位置到文件的结尾处(append,以追加方式写)
fd = open("foo.txt", O_RDONLY, 0); // 以只读方式打开一个已存在文件
fd = open("foo.txt", O_WRONLY|O_APPEND, 0); // 打开一个已存在文件,并在后面添加一些数据
  1. mode指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为mode & ~umask

      可读 可写 可执行
    user S_IRUSR S_IWUSR S_IXUSR
    group S_IRGRP S_IWGRP S_IXGRP
    other S_IROTH S_IWOTH S_IXOTH
/*
 * 创建新文件,文件拥有者有读写权限,而其他用户都有读权限
 */
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH

umask(DEF_UMASK); // 设置进程的umask
/*
 *   mode & ~umask
 * = DEF_MODE & ~DEF_UMASK
 * = 110.110.110 & ~000.010.010
 * = 110.110.110 & 111.101.101
 * = 110.100.100
 */
fd = open("foo.txt", O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);

一个程序可以同时打开的文件数有限制(典型为20个左右,参见<sys/param.h>NOFILE)。进程通过调用close关闭一个打开的文件(即终止文件名和文件描述符之间的联系,释放文件描述符供其它文件使用)。

#include <unistd.h>

int close(int fd); // 成功返回0, 失败返回-1

关闭一个已关闭的描述符会出错。

若程序通过exit而终止或从主程序中返回将关闭所有打开的文件。

系统调用unlink从文件系统中删除文件

读和写文件

应用程序通过分别调用readwrite来执行读写操作。

#include <unistd.h>

// 成功返回读的字节数,若EOF则为0,若出错返回-1
ssize_t read(int fd, void *buf, size_t n);

// 成功返回写的字节数,若出错则为-1
ssize_t write(int fd, const void *buf, size_t n);

read从描述符为fd的当前文件位置拷贝至多n个字节到存储器位置bufwrite从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置。

size_t被定义为unsigned int,而为在出错时返回-1,ssize_t被定义为int。返回-1的可能性使得read的最大值减小一半,从4GB减小到2GB

在某些情况下,readwrite传送的字节比应用程序要求的少。这些不足值(short count)不表示错误。出现这种情况的原因如下:

  • 读时遇到EOF: 假设我们准备读一个文件,该文件从当前位置开始只含有20个字节,而我们以50个字节的片进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回不足值0来发出EOF信号。
  • 从终端读文本行: 如果打开的文件是与终端相关联的(如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和写网络套接字(socket)。

随机访问

文件的I/O操作通常是顺序的:每次在文件中的读和写都紧接在上一次操作之后,然而,如果必要,一个文件可以按任意顺序读写。

#include <unistd.h>

// 返回新的绝对位置,出错返回-1
off_t lseek(int fildes, off_t offset, int whence);

lseek使得文件描述符所指定的文件的读写位置进行移动:

  • whence: SEEK_SET (the file offset shall be set to offset bytes)
  • whence: SEEK_CUR (the file offset shall be set to its current location plus offset)
  • whence: SEEK_END (the file offset shall be set to the size of the file plus offset)
// 定位到文件尾
lseek(fp, 0L, SEEK_END);

// 重新返回到开始
lseek(fp, 0L, SEEK_SET);

// 取得当前位置
off_t pos = lseek(fp, 0L, SEEK_CUR);

使用lseek,可以像处理一个大数组一样处理文件,代价是访问速度变慢。

读取文件元数据

应用程序能够通过调用statfstat函数,检索到关于文件的信息。

#include <unistd.h>
#include <sys/stat.h>

// 成功返回0,失败返回-1
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

stat以文件名作为输入,fstat以文件描述符作为输入,调用它们都会填写如下所示的一个stat数据结构中的各个成员。

stat结构是i节点(innode)的一部分,该结构定义在<sys/stat.h>

/* Metadata returned by the stat and fstat function */
struct stat {
    dev_t st_dev;     /* Device of inode */
    ino_t st_ino;     /* inode number */
    mode_t st_mode;   /* Protection and file type (mode bite)*/
    nlink_t st_nlink; /* Number of hard links to file */
    uid_t st_uid;     /* User ID of owner */
    gid_t st_gid;     /* Group ID of owner */
    dev_t st_rdev;    /* Device type (if inode device) */
    off_t st_size;    /* Total size, in bytes */
    unsigned long st_blksize; /* Blocksize for filesystem I/O */
    unsigned long st_blocks;  /* Number of blocks allocated */
    time_t st_atime;  /* Time of last access (read) */
    time_t st_mtime;  /* Time of last modification (written or created)*/
    time_t st_mtime;  /* Time of last change */
}

st_size包含了文件的字节数大小。

st_mode则编码了文件访问许可位(同read函数的mode参数)和文件类型。

Unix识别大量不同文件类型:

  • 普通文件: 包含某种类型的二进制或文本数据。对内核而言,文本文件和二进制文件毫无区别。
  • 目录文件: 包含关于其他文件的信息。
  • 套接字: 是一种用来通过网络和其他进程通信的文件。

Unix提供的宏指令根据st_mode来确定文件的类型:

  • S_ISREG(): 是普通文件吗?
  • S_ISDIF(): 是目录文件吗?
  • S_ISSOCK(): 是一个网络套接字吗?