目录
1. Linux文件管理方式
1.1. 静态文件和inode
文件没有打开时,输入静态文件,存储在硬盘上,或者其他设备上,硬盘最小存储单位为“扇区”(Sector),每个扇区时512字节,文件读取时,不会一个扇区一个扇区的读,这样太慢了,而是一次性读取多个扇区,即block,一个block通常为4KB。
那我们读取文件时,如何定位到“block”呢?
我们磁盘进行分区、格式化的时候会将磁盘分成两个区域
- 数据区: 存储文件中的数据
- inode区: 存放inode table,inode表中存放的是一个个的inode节点,不同的inode表示不同的 文件,每一个文件,必须对应一个Inode,inode实际是个结构体,这个结构体重记 录很多信息,如:文件字节大小、文件所有者、文件读写执行权限、文件时间戳 (创建时间、更新时间)文件类型、文件数据存储的block块位置....
inode表本身也占用磁盘空间,每个文件都有唯一的inode,每个inode都有一个与之对应的数字编号,通过这个编号就能在inode表中找到相应的inode
我们Win下对U盘快速格式化时,其实就是删除了U盘的inode table真正存储文件的的数据区域并没有动,所以快速格式化后,数据是可以找回的。
通过前面的介绍,打开文件系统会做下面事情
- 找到这个文件的inode号
- 通过inode号从inode table中找到对应的inode结构体
- 通过inode结构体中记录的数据,确定文件所在的blcock,并读出数据
1.2. 文件打开时的状态
当我们打开一个文件时,内核会申请一段内存(一段缓冲区),并将静态文件的数据从磁盘读取到这段内存中(内存中这份数据成为动态文件),打开文件后,所有对这个文件的读写操作,都是在这份动态文件中进行的。
实际中:
- 打开大文件会比较慢
- 文档写一半,突然断电,内容会丢失
为什么这么设计?
因为,磁盘等都是Flash块设备,块设备的数据读取,哪怕一个字节的改动,也会进行一个block的读写,如果每次修改内容修改后都写回去,就会很不灵活,效率低,而内存中就会很灵活。
在linux中,内核会为每个进程设置一个专门的数据结构用于管理该进程,即PCB:Process contral Block,PCB中有一个指针指向文件描述符表File descriptors,文件表也是一个结构体,其中记录了很多文件相关的信息,如:
- 文件状态标志
- 引用计数
- 当前文件读写偏移量
- inode指针(指向该文件对应的inode)
- ...
进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每个文件描述符都会指向一个对应的文件表:
2. 空洞文件
我们知道我们可以通过lseek可以修改当前文件的读写位置偏移量,此函数允许偏移超过文件长度。
如test文件,大小为4096字节,可以通过lseek偏移到距离头部6000字节位置,然后使用write进行写入,因此4096 ~ 9000之间出现一个空洞。
空洞部分实际不会占用实际物理空间,直到某个时刻进行写入,才会分配对应空间,但是空洞文件形成时,逻辑上,文件大小是包括这部分空洞大小的。
空洞文件有啥用?
- 多线程共同操作文件
(如,创建一个很大的文件,单线程从头写,很慢,多线程分开写,快)
- 迅雷下载,还未下载完成,就发现该文件已经占据了全部文件大小空间,这就是空洞文件,有了空洞文件,多线程就可以从不同位置同时写入
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int fd;
int ret;
char buffer[1024];
int i;
/* 打开文件 */
fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
ret = lseek(fd, 4096, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 初始化 buffer 为 0xFF */
memset(buffer, 0xFF, sizeof(buffer));
/* 循环写入 4 次,每次写入 1K */
for (i = 0; i < 4; i++) {
ret = write(fd, buffer, sizeof(buffer));
if (-1 == ret) {
perror("write error");
goto err;
}
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
示例代码中,open 新建了 hole_file,在 Linux 系统中,新建文件大小是 0,也就是没有任何数据写入。
此时使用lseek函数将读写偏移量移动到4K字节处,再使用write函数写入数据0xFF, 每次写入 1K,一共写入 4 次,也就是写入了 4K 数据,也就意味着该文件前 4K 是文件空洞部分,而后 4K数据才是真正写入的数据
使用 ls 命令查看到空洞文件的大小是 8K,使用 ls 命令查看到的大小是文件的逻辑大小。
使用 du 命令查看空洞文件时,其大小显示为 4K,du 命令查看到的 大小是文件实际占用存储块的大小
3. 多次打开同一文件
同一个文件允许在不同进程中打开,也允许在同一个进程中被多次打开,每次打开的文件描述符均不同。
fd1 = open("./test_file", O_RDWR);
fd2 = open("./test_file", O_RDWR);
fd3 = open("./test_file", O_RDWR);
- 一个进程多次open同一个文件,内存中不会存在多份动态文件
测试:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buffer[4];
int fd1, fd2;
int ret;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* 通过 fd1 文件描述符写入 4 个字节数据 */
buffer[0] = 0x11;
buffer[1] = 0x22;
buffer[2] = 0x33;
buffer[3] = 0x44;
ret = write(fd1, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err2;
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd2, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
memset(buffer, 0x00, sizeof(buffer));
ret = read(fd2, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
buffer[2], buffer[3]);
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}
fd1写入4个字节,那如果内存中只存在一份动态文件,那fd2就会读出这写入的4个字节
- 一个进程多次open同一个文件,不同文件描述符所对应的读写位置偏移是互相独立的
同一个文件被多次打开,会得到不同的文件描述符,也就表示,会有多个不用的文件表,而文件的读写偏移量就记录在文件表数据结构中,所以是独立的,如上图所示。
多个不同进程中调用open打开同一个文件,同样内存中只维护一份动态文件,多个进程间共享,他们有各自独立的读写偏移
动态文件何时关闭?当文件引用计数为0系统会自动关闭。
4. 复制文件描述符
同一进程连续多次打开同一文件,如果还需要接续写的话,可以通过设置O_APPEND标志进行,也可以通过复制文件描述符进行,为什么呢?
linux中open得到的fd时可以进行复制的,复制成功后,会得到一个新的描述符,两个文件描述符均可以对文件进行操作,且拥有相同的权限。
这个图就能描述为什么复制文件描述符能实现续写 ,因为两者共享了文件偏移量。
- dup
- dup2
5. 文件共享
常见的三种文件共享实现方式
- 同一进程多次调用open打开同一文件,各个数据结构之间关系如下图:
这种情况非常简单,多次调用open会产生多个文件描述符,且多个文件描述符对应多个文件表,所有文件表索引到同一个Inode节点,即磁盘上同一个文件。
- 不同进程分别使用open打开同一文件,数据结构如下:
与前面类似,只是文件描述符归属于不同的PCB管理
- 同一进程使用dup(dup2)复制文件描述符,数据结构如下:
6. 原子操作与竞争冒险
下图可以简略描述两个进程间的竞争冒险问题:
如何消除这种问题呢?使用原子操作
上述问题出现的原因,在于逻辑:先定位到文件末尾,再写,这是两个动作,如果将这两个动作原子化,就不会出现上述问题
如何实现?
- O_APPEND 实现原子操作,O_APPEND 默认就是原子操作
- pread()和 pwrite()
pread和pwrite都是系统调用,与read、write作用一样,
调用pread相当于调用lseek后再调用read,pwrite同理。