参考书籍《深入理解计算机系统》
Unix下所有的I/O设备,例如网络、磁盘、终端等都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。这种架构使得所有的输入和输出都能以一种统一且一致的方式执行。
1 打开和关闭文件
- 打开文件
//在linux终端通过 man 2 open 查看详情
#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:以按位或的方式进行状态组合,必须包含一个访问模式
1.访问模式标志:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
2.文件创建标志:影响文件打开时的操作
O_CLOEXEC:启动FD_CLOEXEC标志,避免额外调用fcntl的F_SETFD操作设置FD_CLOEXEC标志。FD_CLOEXEC是在进程执行exec系统调用时关闭此打开的文件描述符,防止父进程泄露打开的文件给子进程。原子操作;
O_CREAT:如果文件不存在,则创建一个常规文件;
O_DIRECTORY:如果目标路径不是目录,则打开失败,可以用来测试目标路径是不是目录;
O_EXCL:确保此次调用会创建新文件,如果存在 O_CREAT 标志,并且目标路径文件已存在,则打开失败,错误为EEXIST;如果文件存在且正在被系统使用,则失败,错误为EBUSY;
O_NOCTTY:防止打开文件称为程序控制终端;
O_NOFOLLOW:如果目标路径是一个链接,则打开失败,错误为ELOOP;
O_TMPFILE:创建一个无名的普通文件,此无名节点不会在目录系统中被创建,除非给这个文件赋予了名字,否则所有写入此文件的输入,都将在此文件关闭后丢失;
O_TRUNC:如果文件已存在,且是普通文件,则文件内容将被清空(length为0),如果是FIFO或终端,此标志将被忽略;此外其他文件的操作不可预料;
3.文件状态标志:影响后续I/O输入输出操作
O_APPEND:以附加模式打开文件,默认输入会追加在文件结尾,如果同时有多个进程在附加数据,可能会导致NFS文件系统下的文件损坏;
O_ASYNC:使用信号驱动I/O,当此文件描述符能够输入/输出时生成一个信号(默认是SIGIO,可以通过fcntl()函数修改信号),此标准只适用于终端(terminals)、伪终端(pseudoterminals)、套接字(sockets)、管道(pipes)和FIFO;
O_DIRECT:尽量减少缓存影响,这会降低一部分性能;
O_DSYNC:写操作会同步等待I/O操作完成,但是如果写操作不影响读取刚写入的数据,则不等待文件属性更新;
O_LARGEFILE:允许大小超过off_t(4字节)但是小于0ff64_t的文件被打开;
O_NOATIME:不更新文件记录(访问时间等),能提升I/O效率,多用于只读文件;
O_NONBLOCK / O_NDELAY:采用非阻塞模式打开文件,后续文件描述符的任何操作都不会导致程序阻塞;
O_PATH:并未真正打开文件,只提供了文件描述符,后续能执行一些描述符层面的操作;
O_SYNC:写操作会同步等待I/O操作完成,包括write引起的文件属性的更新;
mode:只在开启 O_CREAT 或者 O_TMPFILE 标志时有用,其他时候将被忽略
S_IRWXU:00700 用户有读、写和驱动权限
S_IRUSR:00400 用户有读权限
S_IWUSR:00200 用户有写权限
S_IXUSR:00100 用户有驱动权限
S_IRWXG:00070 同组用户有读、写和驱动权限
S_IRGRP:00040 同组用户有读权限
S_IWGRP:00020 同组用户有写权限
S_IXGRP:00010 同组用户有驱动权限
S_IRWXO:00007 其他用户有读、写和驱动权限
S_IROTH:00004 其他用户有读权限
S_IWOTH:00002 其他用户有写权限
S_IXOTH:00001 其他用户有驱动权限
返回值:
成功则返回一个小的非负的正整数,用来表示打开的文件(称之为文件描述符),返回的文件描述符是在进程中当前没有打开的最小描述符;
失败则返回-1
- 关闭文件
注意:文件描述符被成功关闭,并不等价于数据已经被成功写入到磁盘中,内核通常会用一块缓存来推迟写入磁盘。你可以调用fsync()
确保数据已经写入磁盘。
#include <unitstd.h>
功能:关闭一个文件描述符,并删除其记录锁(record locks)
原型:
int close (int __fd);
参数:需要被关闭的文件描述符
返回值:成功返回0;失败返回-1,同时失败原因将设置到errno
常见错误:定义于<errno-base.h>和<errno.h>
#define EINTR 4 //close调用被信号中断
#define EIO 5 //IO错误
#define EBADF 9 //描述符(__fd)不是一个不是打开并且有效的状态
#define ENOSPC 28 //设备空间不足
2 读和写文件
- 读
#include <unitstd.h>
功能:从描述符(__fd)的当前文件位置,拷贝至多__nbytes个字节到__buf指定位置
原型:
ssize_t read (int __fd, void *__buf, size_t __nbytes);
返回值:成功返回读到的字节数;失败则返回-1,同时失败原因将设置到errno
常见错误:定义于<errno-base.h>和<errno.h>
#define EINTR 4 //close调用被信号中断
#define EIO 5 //IO错误
#define EBADF 9 //描述符(__fd)不是一个不是打开并且有效的状态
#define EAGAIN 11 //再试一次,同EWOULDBLOCK,常见于套接字(socket),文件描述符被设置为非阻塞(O_NONBLOCK),通常代表此次读取没有数据
#define EFAULT 14 //__buf为不可访问地址
#define EISDIR 21 //描述符对象是一个目录
#define EINVAL 22 //无效参数,如:描述符不是一个可以读的对象、__buf是特殊地址等
- 写
#include <unitstd.h>
功能:从__buf指定地址的赋值__n个字节到__fd描述的对象中去
原型:
ssize_t write (int __fd, const void *__buf, size_t __n);
返回值:成功返回写入的字节数;失败则返回-1,同时失败原因将设置到errno
常见错误:定义于<errno-base.h>和<errno.h>
#define EPERM 1 //操作不被允许
#define EINTR 4 //close调用被信号中断
#define EIO 5 //IO错误
#define EBADF 9 //描述符(__fd)不是一个不是打开并且有效的状态
#define EAGAIN 11 //再试一次,同EWOULDBLOCK,常见于套接字(socket),文件描述符被设置为非阻塞(O_NONBLOCK)
#define EFAULT 14 //__buf为不可访问地址
#define EINVAL 22 //无效参数,如:描述符不是一个可以读的对象、__buf是特殊地址等
#define EFBIG 27 //要写入的字节太大了
#define ENOSPC 28 //设备空间不足
#define EDESTADDRREQ 89 //描述符(__fd)指向尚未connect设置通信地址的datagram套接字
#define EDQUOT 122 //磁盘块配额已用完
#define EPIPE 32 //被关闭的管道(pipe)
3 读取文件信息(元数据)
应用程序能通过以下接口,检索到文件的信息,包括文件的字节数大小等。
#include <unistd.h>
#include <sys/stat.h>
函数原型:
int stat (const char *__restrict __file, struct stat *__restrict __buf);
int fstat (int __fd, struct stat *__buf);
stat定义(x86_64为例):
struct stat
{
__dev_t st_dev; /* Device. */
__ino_t st_ino; /* File serial number. */
__nlink_t st_nlink; /* Link count. */
__mode_t st_mode; /* File mode. */
__uid_t st_uid; /* User ID of the file's owner. */
__gid_t st_gid; /* Group ID of the file's group.*/
int __pad0;
__dev_t st_rdev; /* Device number, if device. */
__off_t st_size; /* Size of file, in bytes. */
__blksize_t st_blksize; /* Optimal block size for I/O. */
__blkcnt_t st_blocks; /* Number 512-byte blocks allocated. */
__time_t st_atime; /* Time of last access. */
__syscall_ulong_t st_atimensec; /* Nscecs of last access. */
__time_t st_mtime; /* Time of last modification. */
__syscall_ulong_t st_mtimensec; /* Nsecs of last modification. */
__time_t st_ctime; /* Time of last status change. */
__syscall_ulong_t st_ctimensec; /* Nsecs of last status change. */
__syscall_slong_t __glibc_reserved[3];
};
4 共享文件
- 内核表示打开的文件的方式
1.描述符:每个进程都有独立的描述符表,其表项由进程打开的文件描述符来索引,每个打开的描述符表项指向文件表中的一个表项;
2.文件表:打开的文件的集合是由一张文件表来表示的,所有的进程共享这张表,每个文件表的表项组成包括当前文件位置、引用计数、一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数,内核直到其引用计数为零,才会删除这个文件表项;
3.v-node表:所有进程共享此表,每个表项包含stat结构中的大多数信息。 - 原理:如下图,多个描述符通过不同的文件表表项来引用同一个文件
5 标准I/O
- 简介
ANSIC定义了一组高级输入输出函数,称为标准I/O库,为程序员提供了Unix I/O的较高级别接口。标准I/O库将一个打开的文件模型化为一个流,对于程序员而言,一个流就是一个指向类型为FILE结构的指针,每个ANSI C程序开始时都有三个打开的流:stdin、stdout、stderr,分别对应与标准输入、标准输出和标准错误。 - 接口
#include <stdio.h>
打开文件
FILE *fopen (const char *__restrict __filename, const char *__restrict __modes);
FILE *fdopen (int __fd, const char *__modes);
关闭文件
int fclose (FILE *__stream);
读
size_t fread (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream);
int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...);
int sscanf (const char *__restrict __s, const char *__restrict __format, ...);
char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream);
写
size_t fwrite (const void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __s);
int fprintf (FILE *__restrict __stream, const char*__restrict __format, ...);
int sprintf (char *__restrict __s, const char *__restrict __format, ...);
int fputs (const char *__restrict __s, FILE *__restrict __stream);
更新用户空间缓存数据
int fflush (FILE *__stream);
搜索流中的某个位置
int fseek (FILE *__stream, long int __off, int __whence);