第三章:深入探究文件IO

目录

1. Linux文件管理方式

1.1.  静态文件和inode

1.2.  文件打开时的状态

 2. 空洞文件

3. 多次打开同一文件

 4. 复制文件描述符

5. 文件共享

6. 原子操作与竞争冒险


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同理。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值