3. 深入探究文件 IO
- 1. Linux 系统如何管理文件
- 1.1 静态文件与inode
- 1.2 文件打开时的状态
- 2. 返回错误处理与errno
- 2.1 strerror 函数
- 2.2 perror 函数
- 3. 空洞文件
- 4. O_APPEND 和 O_TRUNC
- 5. 多次打开同一个文件
- 6. 复制文件描述符
- 6.1 dup
- 6.2 dup2
- 7. 文件共享
- 7.1 同一个进程中多次调用 open 函数打开同一个文件
- 7.2 不同进程分别使用 open 函数打开同一个文件
- 7.3 同一个进程中对文件描述符进行复制
- 8. 原子操作与竞争冒险
- 8.1 O_APPEND
- 8.2 pread 和 pwrite
- 8.3 创建一个文件
- 9. fcntl 和 ioctl
- 9.1 fcntl
- 9.2 ioctl
- 10. 截断文件
1. Linux 系统如何管理文件
1.1 静态文件与inode
文件没有被打开的情况下一般都是存放在磁盘中的,并且以一种固定的形式进行存放,这时称为静态文件。文件存储在硬盘上,硬盘的最小存储单元叫做扇区,每个扇区大小是 512 字节, 相当于 0.5k。操作系统读取硬盘的时候不会一个个扇区的读取,而是一次读取多个扇区,也就是一个块, 通常是 4k,也就是 8 个扇区。将磁盘进行分区格式化的时候会分为数据区和 inode 区,inode 区存放 inode 表,该表中存放着 inode 节点,每个节点都是一个结构体,包含着对应文件的属性信息。所以查找文件时,先根据文件名找到对应的 inode 编号,然后找到对应的表,最后查找相关信息,读取数据。
1.2 文件打开时的状态
调用 open 打开文件的时候,内核会申请一段内存,将数据读取到内存中进行管理,也就是动态文件。对动态文件进行读写操作时,和静态文件不会同步,数据的同步由内核完成,内核会在之后将内存这份动态文件同步到磁盘中。静态文件有多个块,一个块有多个扇区,一个扇区有多个字节,所以对静态文件操作时需要反复读写块,而内存可以直接一个字节一个字节的改动,所以速率较快。
在 Linux 系统中,内核会有一个专门的数据结构管理一个进程,叫做 PCB。该结构体中有一个指针指向了文件描述符表,而文件描述符表中的每一个元素对应文件表,文件表存放着文件的相关信息。
2. 返回错误处理与errno
当发生错误时,操作系统会将错误对应的编号赋值给 errno 变量,每个进程都有一个自己的 errno 全局变量
2.1 strerror 函数
#include <string.h>
char *strerror(int errnum); // 参数就是对应的errno,返回对应错误编号的字符串描述信息
2.2 perror 函数
#include <stdio.h>
void perror(const char *s); // 参数可以传递自己想要的信息,然后错误描述就会打印在s之后
3. 空洞文件
lseek 函数还允许文件偏移量超出文件长度,也就是文件末尾还可以向后偏移。比如一个文件只有 4096k,此时在文件头部向后偏移 6000 字节,然后在这里写入数据,那么 4096 ~ 6000 这部分就是空洞。文件空洞部分不会占用任何物理空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的。 文件空洞在多线程共同操作文件时有很大作用,可以将文件分段,不同线程在不同空洞部分写入数据。
4. O_APPEND 和 O_TRUNC
O_TRUNC 会将文件原本的内容清除,然后再写入数据,而O_APPEND 是在文件末尾写入数据。
5. 多次打开同一个文件
一个进程内多次打开一个文件,那么会得到多个不同的文件描述符,同理在关闭的时候需要依次关闭对应的文件描述符。而且在内存中不会存在多份动态文件,不同文件描述符对应的读写位置偏移量是相互独立的。因为位置偏移量是相互独立的,所以对不同的文件描述符读写时,是分别进行读写。使用 open 函数打开文件时,默认是覆盖式写入,也就是说当分别进行写入操作时,后续写入数据时会先将文件清空再写入。 不同的文件描述符就对应不同的文件表,而位置偏移量就保存在文件表中,但是文件表中的 inode 指针指向的都是同一个 inode。
同样,多个不同的进程打开同一个文件,在内存中也只是维护一份动态文件,多个进程间共享,有各自独立的文件读写位置偏移量。当文件的引用计数为 0 时,系统会自动关闭文件。
6. 复制文件描述符
在 Linux 系统中,open 得到的文件描述符可以进行复制,新的文件描述符也可以对旧文件描述符指向的文件进行操作,拥有相同的权限。但是新的文件描述符和旧的文件描述符指向的文件表是同一个
6.1 dup
#include <unistd.h>
int dup(int oldfd); // 成功返回由系统分配的新的文件描述符,失败返回-1
#include <stdio.h>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd1=open("./text.txt",O_CREAT|O_RDWR,0777);
int fd2=dup(fd1);
write(fd1,"hello ",6);
write(fd2,"world!",6);
return 0;
}
6.2 dup2
#include <unistd.h>
int dup2(int oldfd,int newfd); // 成功返回newfd,失败返回-1
int main()
{
int fd1=open("./text.txt",O_CREAT|O_RDWR,0777);
int fd2=dup2(fd1,9); // 可以指定文件描述符
write(fd1,"hello ",6);
write(fd2,"world!",6);
cout << fd2<<endl;
return 0;
}
7. 文件共享
7.1 同一个进程中多次调用 open 函数打开同一个文件
会得到同一个文件的不同文件描述符,并且多个文件描述符对应多个不同的文件表,所有的文件表指向同一个 inode 节点
7.2 不同进程分别使用 open 函数打开同一个文件
7.3 同一个进程中对文件描述符进行复制
8. 原子操作与竞争冒险
当两个独立的进程对同一个文件进行操作时,因为此时文件是共享的,如果当一个进程的操作未完成时,另一个进程就对文件进行操作就会发生竞争冒险。所以就有了原子操作。原子操作是指一个任务要么不做,要么做完。
O_APPEND、pread() 和 pwrite()、创建文件都是可以实验原子操作的。
8.1 O_APPEND
两个进程都向文件中写入数据,后一个进程会覆盖前一个进程写入的内容,就需要使用该标志
8.2 pread 和 pwrite
这两个函数可传入一个位置偏移量,用于指定文件当前读写的位置偏移量。但是不更新文件表中当前位置偏移量,就是说在当前位置 0 调用这两个函数时如果设置 offset 为1024,然后再调用 lseek 获取当前位置,发现依旧是 0
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
8.3 创建一个文件
如果两个进程都会创建同一个文件,就需要使用 O_EXCL,如果要打开的文件已经存在,就 open 失败,如果不存在,就创建这个文件
9. fcntl 和 ioctl
9.1 fcntl
fcntl 函数可以对一个已经打开的文件描述符执行一系列控制操作
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd,.../* arg */);
/* cmd:操作命令,表示我们将对fd进行什么操作
* F_DUPFD 或 F_DUPFD_CLOEXEC :复制文件描述符
* F_GETFD 或 F_SETFD :获取/设置文件描述符标志
* F_GETFL 或 F_SETFL :获取/设置文件状态标志
* F_GETOWN 或 F_SETOWN :获取/设置异步IO所有权
* F_GETLK 或 F_SETLK :获取/设置记录锁
* /
// 第三个参数根据cmd来传入对应的实参
// 返回值是失败返回-1,成功根据cmd有不同的返回值
复制文件描述符
int main()
{
int fd1=open("./test.txt",O_CREAT|O_RDONLY,0777);
int fd2=fcntl(fd1,F_DUPFD,0);// 如果传入的第三个参数已经被使用,就返回一个比0大的可使用的文件描述符,否则就返回第三个参数
cout << fd2 << endl;
return 0;
}
获取/设置文件状态标志
int main()
{
int fd1=open("./test.txt",O_CREAT|O_RDWR,0777);
int flag=fcntl(fd,F_GETFL);
int fd2=fcntl(fd1,F_SETFL,flag|O_APPEND);
cout << fd2 << endl;
return 0;
}
F_GETFL 成功时返回状态标志,F_SETFL 的第三个参数表示需要设置的状态标志。但是文件权限标志(O_RDONLY、 O_WRONLY、 O_RDWR)以及文件创建标志(O_CREAT、O_EXCL、 O_NOCTTY、 O_TRUNC)不能被设置,只有 O_APPEND、 O_ASYNC、O_DIRECT、 O_NOATIME 以及 O_NONBLOCK 这些标志可以被修改
9.2 ioctl
可以认为是文件 IO 操作的杂物箱,一般用于操作特殊文件或硬件外设,比如获取 LCD 相关信息等,这里只是介绍以下
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request,...);
// request 表示向文件描述符请求相应的操作,第三个可变参数根据request设置
10. 截断文件
使用系统调用 truncate() 或 ftruncate()
可将普通文件截断为指定字节长度
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);// 使用前必须open,并且有可写权限
先创建两个文件,这时文件大小都是0字节
向文件中插入数据,改变文件大小
int main()
{
int fd1=open("./file1",O_RDWR);
char buffer1[4096]={0};
char buffer2[2048]={0};
write(fd1,buffer1,sizeof(buffer1));
write(open("./file2",O_RDWR),buffer2,sizeof(buffer2));
return 0;
}
截断文件,发现文件大小改变了
int main()
{
int fd1=open("./file1",O_RDWR);
char buffer1[4096]={0};
char buffer2[2048]={0};
write(fd1,buffer1,sizeof(buffer1));
write(open("./file2",O_RDWR),buffer2,sizeof(buffer2));
ftruncate(fd1,1024);
truncate("./file2",2048);
return 0;
}