10 系统级IO
输入/输出(I/O)是在主存和外部设备之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。在Linux系统中,是通过使用由内核提供的系统级Unix I/O函数来实现较高级别的I/O函数的。
Unix I/O¶
一个Linux文件就是一个m个字节的序列:B_0, B_1, ..., B_k, ..., B_{m-1}。所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。Linux内核使用一个简单、低级的应用接口即Unix I/O来统一且一致的执行所有的输入和输出:
- 打开文件。一个应用程序通过要求内核打开相应的文件,内核返回一个小的非负整数,称为描述符。内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
- Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
- 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
- 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k\ge m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的"EOF符号"。类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
文件¶
每个Linux文件都有一个类型(type)来表明它在系统中的角色:
- 普通文件(regular file)包含任意数据。
- 目录(directory)是包含一组链接(link)的文件,其中每个链接都将一个文件名映射到一个文件。每个目录至少包含两个条目:
.
是到该目录自身的链接,以及..
是到父目录(parent directory)的链接。 - 套接字(socket)是用来与另一个进程进行跨网络通信的文件。
其他文件类型包含命名管道(named pipe)、符号链接(symbolic link)等。
Linux内核将所有文件都组织成一个目录层次结构(directory hierarchy),由名为/的根目录确定。下图显示了Linux系统的目录层次结构的一部分。
打开和关闭文件¶
进程是通过调用open
函数来打开一个已存在的文件或者创建一个新文件的。
int open(char *filename, int flags);
int open(char *filename, int flags, mode_t mode);
open
函数将filename
转换为一个文件描述符并返回。返回的描述符总是在进程中当前没有打开的最小描述符。flags
参数指明了进程打算如何访问这个文件:
O_RDONLY
只读、O_WRONLY
只写、O_RDWR
可读可写,O_EXEC
只执行O_APPEND
追加、O_CREAT
创建、O_TRUNC
截断O_SYNC
:把数据写入内核空间缓冲,一直阻塞直到数据从缓冲写入到存储设备O_DIRECT
: 绕过内核空间缓冲,从用户空间直接写入到存储设备,一般和O_SYNC
一起使用确保同步
mode
参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask
。umask
与chmod
的效果刚好相反,设置的是权限"补码"。当进程通过带某个mode
参数的open
函数调用来创建一个新文件时,文件的访问权限位被设置为mode & ~ umask
。
umask
用户登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask
干的事情。文件的默认创建权限是666和目录777。要计算新文件的权限,请从默认值中减去umask值。例如,要计算unask 022
将如何影响新创建的文件和目录:
- 文件:666 - 022 = 644。所有者可以读取和修改文件。 组和其他人只能读取文件。
- 目录:777 - 022 = 755。所有者可以进入目录并列出读取,修改,创建或删除目录中的文件。 组和其他人可以进入目录并列出并读取文件。
下面是fopen()
中mode
和open()
中flags
的对比1
fopen() Mode |
open() Flags |
---|---|
r |
O_RDONLY |
w |
O_WRONLY |
a |
O_WRONLY |
r+ |
O_RDWR |
w+ |
O_RDWR |
a+ |
O_RDWR |
如果需要创建文件,也可以使用create
:
int creat(const char *path, mode_t mode);
等效于:
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
进程通过调用close
函数关闭一个打开的文件。
int close(int fd);
读和写文件¶
应用程序是分别调用read
和write
函数来执行输入和输出的。
# 返回:若成功则为读的字节数,若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);
size_t/ssize_t
在x86-64系统中, size_t
被定义为unsigned long, 而ssize_t
(有符号的大小)被定义为long。
读取文件元数据¶
每个文件的元数据由内核管理,可以通过stat
和fstat
函数访问。
int stat(const char *filename, struct stat *buf)
int fstat(int fd, struct stat *buf)
结构体stat
如下:
/* Metadata returned by the stat and fstat functions */
struct stat {
dev_t st_dev; /* Device */
ino_t st_ino; /* inode */
mode_t st_mode; /* Protection and file type */
nlink_t st_nlink; /* Number of hard links */
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 */
time_t st_mtime; /* Time of last modification */
time_t st_ctime; /* Time of last change */
};
共享文件¶
内核用三个相关的数据结构来表示打开的文件:
- 描述符表(descriptor table): 每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
- 打开文件表(open-file table): 打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括当前的文件位置、引用计数(reference count),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为0。
- v-node表(v-node table):每个表项包含
stat
结构中的大多数信息,包括st_mode
和st_size
成员。所有的进程共享这张表。
多个描述符可以通过不同的文件表表项来引用同一个文件。例如如果以同一个文件名调用open
函数两次,就会发生这种情况。这时每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
父子进程是共享文件的,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。在内核删除相应文件表表项之前,父子进程都必须关闭了它们的描述符。
I/O重定向¶
Linux shell提供了I/O重定向操作符。它使用了dup2
函数,它复制描述符表项oldfd
到描述符表项newfd
,覆盖描述符表项new-fd
以前的内容。
# 返回:若成功则为非负的描述符,若出错则为-1
int dup2(int oldfd, int newfd);
标准I/O¶
C语言定义了一组高级输入输出函数(fopen
/fclose
/fread
/fwrite
等),称为标准I/O库,为程序员提供了Unix I/O的较高级别的替代。标准I/O库将一个打开的文件模型化为一个流,它是对文件描述符和流缓冲区的抽象,其目的是使开销较高的Linux I/O系统调用的数量尽可能得小。
-
https://pubs.opengroup.org/onlinepubs/9699919799/functions/fopen.html ↩