[Linux]文件

  • 如何理解Linux一切皆文件 

外部设备任何数据处理,都必须先把数据放进内存,处理后,把内存中数据刷新到外设中,这个过程是I/O。

键盘有自己的读写方法,操作系统为了管理所有软硬件,进行了先描述再组织。

操作系统利用struct file对硬件进行访问,并保存属性,利用函数指针所保存的不同方法,对硬件进行特定功能的驱动。

站在struct file看,所有设备和文件,统一都是struct file 

 struct file让我们不必关心底层硬件的差别,使用统一的接口,对硬件进行操作。

一个被打开的文件有自己的方法指针,打开这个struct file时会做初始化,指向具体的方法,可以通过struct file找到文件在内核中的缓冲区。 


这个程序运行结果如下 

重定向后,打开文件。 

结果如下。 

c语言接口的被打了两遍。

 为了弄清原因,我们把上面程序的fork()注释掉看看。

此时运行程序,结果如下。

重定向后打开文件。

结果如下。

 可见,这个现象和fork有关。实际也和缓冲区有关。

printf调用时,数据只要写到显示器上,就已经算写到外设上了,就不属于这个父进程了,但如果数据没有写到显示器上,那么这个数据依旧属于父进程,但是调用了printf,数据不一定被写到显示器上。

解决这个问题,我们需要先在下面谈谈缓冲区。


缓冲区的意义是节省进程的数据在内存和磁盘间I/O的时间。

那么我们是何时把数据拷贝到缓冲区的?

我们与其理解fwrite函数是写入到文件中的函数,倒不如把它了解为拷贝函数,是它把数据从进程拷贝到缓冲区或外设中的。

那么缓冲区刷新策略是怎么样的呢?

我们可以把缓冲区理解为快递中转站,只有快递数量达到一定时,这批快递才会发往下一站。

一块数据缓冲区一次全部传输的方式效率最高,而不是多次少量地进行传输。

  • 缓冲区会结合设备特点,指定自己的刷新策略。
  1. 立即刷新,无缓冲。
  2. 行刷新,缓存数据时是行刷新,比如显示器。
  3. 缓冲区满了后再刷新,比如向磁盘文件中输入数据。

缓冲区刷新有两个特殊情况

  1. 用户强制刷新。
  2. 进程退出,缓冲区要进行刷新。

那么缓冲区在哪里呢?

  

c语言接口的被打了两遍。可见缓冲区一定不在内核中,如果在内核中,write的也应该被打印2次。我们用的一种是C语言接口,一种是操作系统接口,如果缓冲区在内核中,那么系统接口也应该出现两次。

我们之前谈论的所有缓冲区都指的是语言层面提供的缓冲区。

当我们进行文件读写时,我们需要用到stdout,stdin,stderr,与它们进行交互,它们的类型是FILE*,FILE是个结构体,它里面有fd,还包括一个缓冲区。

这个文件指针里面有 这个缓冲区。

如果我们没有重定向'>'。

 前三行是向stdout中打印,它缓冲区是行刷新,在fork之前,这三个c的函数已经讲数据打印到显示器(外设)上了,我们的FILE内部,及本进程内部不存在对应的数据了,如果我们进行了重定向,要写入的文件不再是显示器stdout,而是向文件中输入,缓冲区使用的刷新策略是全刷新,之前的3条c函数虽然带了\n,但不足以把stdout缓冲区存满,数据并没有被刷新。当我们执行fork时,stdout属于父进程,创建子进程时,紧接着就是进程退出,父子进程谁先退出不确定,缓冲区要被强制刷新,此时发生写时拷贝,数据最终会显示两份,write没有写两份,是因为上述过程和write无关,write没有FILE,而是用的fd,没有c提供的缓冲区。


  • 进一步理解缓冲区

mystdio.h

#pragma once
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<assert.h>
#include<stdlib.h>
#include<fcntl.h>
#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
#define errno 0
typedef struct _FILE{
    int flags;
    int fileno;
    int cap;
    int size;
    char buffer[SIZE];
}FILE_;
FILE_*fopen_(const char *path_name,const char *mode);
void fwrite_(const  void*ptr,int num,FILE_*fp);
void fclose_(FILE_*fp);

mystdio.c

#include"mystdio.h"
FILE_*fopen_(const char*path_name,const char*mode)
{
  int flags=0;
  int defaultmode=0;
  if(strcmp(mode,"r")==0)
  {
    flags |=O_RDONLY;
  }
  else if(strcmp(mode,"w")==0)
  {
    flags |=O_WRONLY|O_CREAT|O_TRUNC;
  }
  else if(strcmp(mode,"a")==0)
  {
    flags |=O_WRONLY|O_CREAT|O_APPEND;
  }
  int fd=0;
  if(flags&O_RDONLY) fd=open(path_name,flags);
  else fd=open(path_name,flags,defaultmode);
  if(fd<0)
  {
    const char*err=strerror(errno);
    write(2,err,strlen(err));
    
    return NULL;//创建失败返回NULL
  }
  FILE_*fp=(FILE_*)malloc(sizeof(FILE_));
 assert(fp);
  fp->flags=SYNC_LINE;//,默认设置为行刷新
  fp->fileno=fd;
  fp->cap=SIZE;
  fp->size=0;
  memset(fp->buffer,0,SIZE);
  return fp;//打开一个文件,返回FILE*指针
}
void fwrite_(const  void*ptr,int num,FILE_*fp)
{
  //写入到缓冲区中
memcpy(fp->buffer+fp->size,ptr,num);//这里我们不考虑缓冲区溢出的问题
//判断如何刷新
fp->size+=num;
if(fp->flags&SYNC_NOW)
{
  write(fp->fileno,fp->buffer,fp->size);
  fp->size=0;
}
if(fp->flags&SYNC_FULL)
{
  if(fp->size==fp->cap)
  {
    write(fp->fileno,fp->buffer,fp->size);
    fp->size=0;
  }
}
if(fp->flags&SYNC_LINE)
{
  if(fp->buffer[fp->size-1]=='\n')
  {

    write(fp->fileno,fp->buffer,fp->size);
    fp->size=0;
  }
}
}
void fflush_(FILE_ *fp)
{
  if(fp->size>0)
  write(fp->fileno,fp->buffer,fp->size);
}
void fclose_(FILE_*fp)
{
fflush_(fp);
close(fp->fileno);
}

main.c

#include"mystdio.h"
#include<stdio.h>
int main()
{
  FILE_*fp=fopen_("./log.txt","w");
  if(fp==NULL)
  {
    return 1;
  }
  int cnt=10;
  const char*msg="hello ";
  while(1)
  {
    cnt--;
    fwrite_(msg,strlen(msg),fp);
    sleep(1);
    printf("count:%d\n",cnt);
    if(cnt==0)
      break;
  }
  fclose_(fp);
  return 0;
}

结果

 在此处加入\n后

结果

这时候是行缓冲行刷新,所以每打印一行,就会往文件中打印一行,就能实时看到文件内容在增加。 

 进行如下更改后

 结果。


write后数据并不是直接写到磁盘中,而是写到了操作系统内的文件内核缓冲区中,而从内核缓冲区刷新到磁盘的过程由操作系统自己决定。缓冲策略会变得很复杂。

但如果系统断电,内核缓冲区的数据不会存储到磁盘中,而如果是银行等对数据丢失零容忍的机构,就必须解决这个问题,我们需要使用fsync函数。

这样,再在main函数中加入fflush。

 就能实现每fwrite一次,就会往磁盘中存储一次。


  • 如果一个文件没有被打开,该怎么管理?

没有被打开的文件只能在磁盘上。磁盘上面用大量的文件,也是要被管理起来的,放在合适的位置,方便我们随时打开。

做这部分工作的我们称之为文件系统。

  • 磁盘的物理结构

磁盘是计算机中唯一的机械结构,也是一个外设,决定了硬盘访问会很慢。虽然目前家用电脑很少用磁盘,但在企业,磁盘存储依然是主流。

因为固态硬盘有个坏处,一是性价比不高,单位价格存储的容量少。二是它有读写次数的限制,写多了会出现击穿的情况,数据会丢失。

 

 盘面两个面都能存储,两个面都有磁头。一个磁盘可以用多个盘片。

马达控制盘片旋转。磁盘和盘面是不接触的,但相距很近,磁盘必须防止抖动,一旦接触会“刮花”盘面,会导致数据丢失。

  • 磁盘的存储结构

磁盘寻址时,基本单位不是字节也不是比特,而是扇区。扇区大小一般是512字节。

 长的扇区和短的扇区,存储大小都是512字节。它的电子密度不同。

  •  那么在一个单面上如何单位扇区呢?

首先需要确定扇区在哪个磁道,再寻找在磁道的哪个扇区。

磁头来回摆动的过程就是确认在哪一个磁道。盘片旋转时就是在让磁头定位扇区。

  • 多个盘片(磁盘)的情况

磁头是连在一起的,要动一起动。

 那么磁盘如何寻找扇区?

先定位磁道,再定位磁头(盘面),最后定位扇区。

磁盘中定位任何一个扇区,采用的硬件基本的定位方式,叫CHS定位法。

  • 磁盘的逻辑结构

磁盘物理上是圆形的,我们可以把它想象为线性结构。

我们把磁盘可以从逻辑上看做一个数组。

对磁盘做管理相当于对数组进行管理。

 只要知道了这个扇区的下标,就算定位了一个一个扇区。

在操作系统内部,我们把扇区的地址叫LBA地址。

假设一个磁盘有四个盘面,每个盘面有10个磁道,每个磁道有100个扇区,那么这个磁盘总容量为

4*10*100*512个,每一面有1000个扇区。假设一个LBA为123号,那么这个扇区的确定工程大概如下。

为什么操作系统要对存储结构进行逻辑抽象?直接用CHS不行吗?

  1. 便于管理
  2. 不想让操作系统的代码和硬件强耦合,硬件用的是什么不重要,最终都会转换为LBA。硬件变化,不影响操作系统。

虽然磁盘访问数据的基本单位是512字节,但依然很小。

操作系统内的文件系统会定制地进行多个扇区的读取,采用1kb,2kb,4kb为基本单位。

哪怕只想读取或修改一个bit,必须将4kb下载到内存进行读取或修改,如果必要,再写回内存。

一般使用4kb即8个扇区。

  • 局部性原理

当计算机访问某些数据时,那么它附近的数据也大概率会被访问到。

加载4kb一是为了提高I/O效率,另一方面因为局部性原理,也提高了数据命中的效率,一定程度减缓了更多次I/O的过程。它的本质一是数据预加载,二是空间换时间。

内存被划分了4kb大小的空间,这个在内存中称为页框,磁盘中的文件尤其是可执行文件按照4kb的大小划分好块,这个块叫页帧。

编译器会把可执行程序按照虚拟地址空间的方式编好,还按照4kb大小,把数据进行归类,比如代码区有40kb,那么就给我们分配10个4kb大小。


为了对几百G大小的磁盘进行管理,我们需要对磁盘空间进行一定的划分。

我们对磁盘进行下分区。其中Boot Block是开机模块,我们暂不用考虑。

分区

group0到n是对此磁盘空间的分组,比如把磁盘中的5GB空间分到Block group 0组。对每一组我们还会进行如下划分。

分组

Super Block保存的是整个文件系统的信息。比如保存整个分区有多少个组,起始块号是多少,结束块号是多少等等,Super Block虽然在每个group中都有,但是它保存的是整个分区的信息。

既然保存的是整个分区的信息,为什么在每一个分组中都存在呢?

 是为了备份。防止某组的Super Block丢失,丢失时,如果有备份,其他组的能拷贝过来一份。

 日常使用电脑时,开机时有时候会提示有数据丢失是否恢复,这时恢复就有可能是因为某个Super Block有问题,恢复过程就有可能是从别的Super Block拷贝了一份。

所以实际上对于每个分组来说,我们只考虑Super Block的后面部分。

文件等于内容加属性。内容和属性是分开存储的。

其中inode块包含了文件几乎所有的属性,inode是固定大小的。一个文件有一个inode。

文件名并不在inode中存储。

文件内容在Date Block块中进行存储 。data block随着文件类型的变化,大小在变化。

为了区分inode,每一个inode都有一个ID,我们可以用ls -li命令查询此ID.

 分组后,inode table就被创建好了,里面包含了要被使用的inode。

创建一个文件后,系统会找一个空的inode,把这个文件的属性填充到inode中。

Date block中保存的是分组内部所有文件的数据块(4kb为单位),在分组中会有很多块,一个文件也会占用一些块。Date block就用来存储这个文件占用哪些数据块的。即文件数据是放在Date block中的。

inode Bitmap用来查找哪些inode被使用哪些没被使用了。它是inode对应的位图结构。

第几个比特位代表第几个inode,这个比特位是1代表被占用,为0代表没被占用。

inode table中有几个inode,bitmap中就有多少个比特位。

位图中比特位的位置和当前文件的inode的位置一一对应。

Block bitmap是数据块对应的位图结构。

位图中的比特位位置和当前data block对应的数据块位置是一一对应的。

GDT块叫做块组描述表,里面包含对应分组的宏观属性信息。

比如说存储inode有多少,被用了多少,数据库有多少,被用了多少。

而如果我们要查找一个文件,我们要使用这个文件的inode编号。

磁盘中每一个分区inode范围都是不同的。

给一个inode后,先用inode bitmap查看inode是否有效,再通过inode table,查找对应编号的inode,获取文件的属性。

inode中的每个文件有个映射表,映射表包含一个数组,这个数组存储了这个文件数据块都有哪些,数据块也是有编号的。

数据块中有可能存的是其他多个数据块的地址,这样数组中的一个编号可能就会找到很多数据块。

如果要删除一个文件,只需要先把inode bitmap中对应的比特位1置为0,再把block bitmap中文件数据对应的比特位1置为0就行。(惰性删除)

所以一个文件被删除可以被恢复。 先把inode找到,再把inode bitmap中对应的比特位从0变为1,再找到映射表,把block bitmap 中对应数据块对应的比特位从0变为1. 

所以说误删文件后,不要做别的,要直接进行文件恢复操作。否则inode可能就找不到了。

那么我们平时是用文件名对文件操作的,没有用inode,那么我们是怎么利用上的inode呢?

任何一个文件一定在一个目录下,目录也是个文件,有自己的属性和内容,目录也有inode和数据块,文件名并不在inode中存储。目录数据块里面放的是当前目录下各个文件名和本身inode的映射关系。一个目录下不能有相同的文件名。

所以我们在一个目录下创建文件,必须有这个目录的写入权限,因为我们在这个目录下新增文件时,是要在这个目录的内容中写入文件名和inode的映射关系。当我们想要获取目录内容时必须拥有文件读权限,因为我们要通过目录这个文件的数据块根据文件名找到inode

  • 软硬链接

有独立inode的链接文件叫软链接,没有独立inode的链接文件叫硬链接。

 软硬链接区别:是否具有独立的inode,

软链接可以被当成独立文件。

硬链接没有独立inode。

将hello输入到mydir文件后, 硬链接文件和myfile.txt文件大小发生变化。

 建立硬链接,根本没有新增文件。因为inode没有给硬链接分配独立的inode。

它的属性和内容用的就是它所指向的文件(mkdir)的属性和内容。

既然没有创建文件,一定没有自己的属性集合和内容集合,用的一定是别人的inode和内容。

一个文件包括文件名,inode和属性,创建硬链接,inode和属性用的都是别人的,只是在对应的目录下,写入了一个hand_file.link,和指向的文件的inode的映射关系。

创建硬链接本质就是在指定的路径下,新增文件名和inode编号的映射关系。

文件名在同一路径下是唯一的。

inode有可能被多个文件指向,inode有自己的计数器。叫count的引用计数,叫硬链接数。

 硬链接数由1变成2.

当我们删除myfile后,硬链接文件还在,引用计数变成了1.也相当于对原先文件重命名了。

,当一个文件的硬链接数为0时, 一个文件才算被真正地删除。

软链接指向的文件实际是存在的,但删除myfile.txt后,却找不到,可见软链接不是通过inode寻找文件的。

软链接用的是目标文件的文件名寻找的

 软链接是个独立文件,有自己的数据块,只不过,它的数据块包含的是所指向的目标文件的路径。所以目标文件被删除后,软链接就失效了。

新创建一个同名文件,软链接就重新指向它了,但这个是个全新的文件。原先的硬链接和它用的也不是一个inode。

 删除软链接的命令。

 软链接相当于Windows下的快捷方式。

  • 软硬链接有什么用?

软链接主要应用就是类似于快捷方式。

 为什么p平台文件默认硬链接数是1?因为普通文件自己就有文件名和inode的映射关系。

为什么空目录默认硬链接数是2?

 目录中有'.',这个代表当前目录的文件。它是个硬链接。


我们发现,在目录中创建一个目录后,这个目录硬链接数增加了1. 

 因为dir的上级目录文件".."的inode和empty相同。

 为什么cd ..命令能返回上级目录,就是因为..指向上级目录文件。


  • 普通用户为什么不能给目录建立硬链接? 

(4 封私信 / 80 条消息) linux为什么不能硬链接目录? - 知乎 (zhihu.com)


 Access是最后访问的时间。

Modefy是最后更改文件内容的时间。

Change是上次更改文件属性的时间。

注意,一般更改文件内容,文件大小会改变,文件属性就会改变,Change一般会改变。

Access时间不会每次访问后都进行改变,一般达到一定次数后才会改变。

修改内容和属性也算访问。 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南种北李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值