APUE第三章总结:文件IO
这是APUE(Advanced Programming in the UNIX Environment,UINX环境高级编程)阅读笔记系列文章
首先参考一个简单的类UINX操作系统实现——xv6,了解操作系统是如何组织文件资源的
xv6的File IO实现
File是操作系统管理的底层资源
在xv6中,file
结构体是对管道,inode
(表示常规文件),设备的抽象。xv6没有内核内存分配器,只是静态分配了一个固定大小的file
数组,用于存放内核打开的所有file
file descriptor是File的引用
file descriptor(简称fd)在xv6中,是通过PCB(进程控制块)中的一个固定大小的数组存储的,这表明了两点:
- fd是process local的,表示进程对内核资源的引用
- 一个进程能够打开的文件是有最大数量限制的
fd和windows的handler很相似,它是为了避免直接使用指针引用内核数据,优点如下
- 避免内核数据结构暴露在进程中
如果直接将
file
结构体的指针交给进程使用,进程就能知道file
的内存地址,虽然进程一般无法读写这块地址,但不代表这是足够安全的 - 防止恶意进程提供错误指针误导内核
假设进程提供的指针是非法指针,并不指向
file
结构体,这种情况在xv6中能够通过判断指针是否在数组中越界来确定
然而,如果进程提供的指针指向其他进程打开的文件,就有机会滥用其他进程的资源
换而言之,指针是全局的,而fd是process local的 - 向进程屏蔽
file
结构体的实现细节 众所周知随着内核版本的迭代,内核使用的某些数据结构会发生改变,例如添加新功能,需要在原有的结构体上添加新的字段
这样的细节对进程是无用的,还会导致编译后的二进制依赖于某个特定版本的内核实现
stdin, stdout, stderror只是约定
通常一个程序开始运行时,已经有三个文件打开了:0,1,2,分别表示stdin, stdout, stderror
在xv6的实现中,内核并没有对0,1,2特殊处理,因为它只是shell的约定。xv6在用户态下提供了一个shell程序,片段如下
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
dup(0); // stdout
dup(0); // stderr
可以看出shell打开了终端设备文件,并通过dup
使得0,1,2指向同一个终端设备文件。而在shell中运行的程序fork自shell程序,所以继承了shell程序的0,1,2文件描述符
换而言之,一个程序运行时,0,1,2不一定是已经打开的文件!!!
文件打开,关闭
open
open函数打开一个文件,返回fd
int open (const char *__file, int __oflag, ...)
参数名 | 说明 |
---|---|
__file | 文件路径 |
__oflag | 打开方式,通过比特掩码传递flags |
mode | 只有在创建文件时才会使用,用于设置创建的文件的权限,类型为mode_t |
返回 | 文件描述符,-1 表示打开失败,并设置erron ,其他情况返回文件描述符 |
返回的fd总是未使用的fd中最小的一个,利用这个特性可以重定向标准输入,标准输出,标准错误
oflag
常用oflag
宏 | 说明 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWD | 可读可写 |
O_EXEC | 执行 |
O_APPNED | 写入内容附加到末尾 |
O_CLOEXEC | 执行exec系列函数时自动关闭,防止子进程继承到父进程的该文件 |
O_CREATE | 如果文件不存在,自动创建 |
O_EXCL | 和O_CREATE配合使用,如果文件存在,则返回错误 |
O_TRUNC | 如果文件能够以可读方式打开,则将文件大小设置为0 |
close
int close (int __fd)
进程结束时,操作系统会自动关闭进程打开的文件,所以在一些场合下,可以不调用close
关闭文件
creat
vs open
考虑creat
函数
int creat (const char *__file, mode_t __mode)
相比open
,只能创建文件
打开文件时,如果需要自动创建文件,有两种方式
- 使用
open
带O_CREATE
- 使用
open
带O_CREATE|O_EXCL
判断文件是否存在,如果不存在则使用creat
创建。之后再打开文件
这两种方式的不同在于,在并发环境下,先判断一个条件是否成立再执行某个操作,这样的流程是线程不安全的,有可能在判断条件成立后,内核剥夺CPU,重新被调度时条件已经不成立。
使用open
带O_CREATE
,判断文件是否存在和创建文件两个步骤是原子操作,保证不会出现竞态条件
文件操作
xv6实现中,file
字段有一个属性,用于记录文件偏移
read
ssize_t read (int __fd, void *__buf, size_t __nbytes)
参数 | 说明 |
---|---|
fd | 必须是可读的文件描述符,可以是常规文件,终端设备,网络套接字等 |
__buf | 进程准备的缓冲数组,内核会将读取到的文件内容存放到这里 |
__nbytes | 进程期望一次最多读取的字节数量 |
返回 | 实际读取的字节数,-1 表示遇到错误,0 表示遇到EOF |
如果实际读取的字节数小于__nbytes
,表明发生了如下情况
- 如果读取常规文件,则表示读取
__nbytes
个字节时遇到了EOF
- 如果从终端设备中读取,通常一次读取一行,返回这一行的字节数
- 如果从网络套接字中读取,网络缓冲可能导致小于
__nbytes
个字节读取 - 如果从管道中读取,而管道中剩余字节数小于
__nbytes
- etc…
读取后,文件偏移向前移动实际读取的字节数
write
ssize_t write (int __fd, const void *__buf, size_t __n)
参数 | 说明 |
---|---|
fd | 同read |
__buf | 进程准备的缓冲数组,内核会读取这个缓冲数组中的字节并写入文件 |
__nbytes | 进程期望一次最多写入的字节数量 |
返回 | 大部分情况下等于__nbytes |
如果返回值不等于__nbytes
,表明错误发生,可能是磁盘已满,或者用户磁盘使用超过配额
读取后,文件偏移向前移动实际写入的字节数
为了提高IO性能,内核不会立刻将数据写入磁盘,而是先写入内存中的IO缓冲区,随后(保证一定时间内)写入磁盘
如果打开文件时使用了O_SYNC
,会阻塞直到数据落盘
如果打开文件时使用了O_APPEND
,会先将文件偏移移动到文件末尾,再写入
lseek
lseek
函数用于修改文件偏移
__off_t lseek (int __fd, __off_t __offset, int __whence)
参数 | 说明 |
---|---|
__fd | 文件描述符 |
__offset | 新的文件偏移 |
__whence | 宏,表示相对位置 |
返回 | lseek调用后的文件偏移 |
__whence
使用的宏
SEEK_SET
,相对文件开头寻址SEEK_CUR
,相对当前位置寻址SEEK_END
,相对文件末尾寻址
lseek
中的l
表示long integer,在引入__off_t
前,lseek
返回类型是long
off_t
是有符号数,在极特殊情况下可能是负数,而返回-1
又代表错误,所以应该使用==-1
判断是否出现错误而不是<0
不是所有类型的文件都能使用lseek
,对管道,套接字,FIFO使用lseek
会返回-1
表示错误
lseek常见用法
- 判断文件是否可寻址
if (lseek(fd, 0 ,SEEK_CUR) != -1) { // ... }
- 获取当前文件偏移
off_t pos = lseek(fd, 0 ,SEEK_CUR);
- 获取文件大小
off_t size = lseek(fd, 0 ,SEEK_END);
lseek
只修改文件偏移,不会产生IO操作,可以使用lseek
获取大文件的大小
文件打洞
lseek
允许将文件偏移设置到EOF之后,随后调用write
,就能在文件中间打一个洞
文件打洞后,空洞范围内使用read
读取到的值是0
('\0'
),空洞不会增加文件大小,部分文件系统不会存储空洞中重复的数据
dup系列函数
dup系列函数用于实现file
对象的共享,也就是两个fd指向同一个资源,因此,也会共享file
对象的属性,比如file
标志位,文件偏移等等
dup
int dup (int __fd)
参数 | 说明 |
---|---|
__fd | 文件描述符 |
返回 | 一个新的文件描述符,和__fd 指向相同的资源 |
dup
返回的fd总是未使用的fd中最小的一个
dup2
int dup2 (int __fd, int __fd2)
参数 | 说明 |
---|---|
__fd | 文件描述符 |
__fd2 | 期望dup文件描述符的位置 |
如果__fd2
是已经打开的文件描述符,则自动关闭,然后再执行dup操作
如果__fd == __fd2
,则直接返回__fd
如果__fd2
未使用,则dup2
执行完毕后,__fd2
的FD_CLOEXEC
标志位被清除,方便与fork
配合实现管道通信
dup2
vs close
-open
重定向标准输入,也就是在0处打开其他文件,有两种做法 1.
close(0);
open("path/to/file", O_RDONLY);
int fd = open("path/to/file", O_RDONLY);
dup2(fd, 0);
区别是,使用dup2
可以明显地表达意图,可读性更好