0


【Linux篇】第十篇——基础IO(系统文件IO+文件描述符+重定向+文件系统+软硬链接)

C语言文件IO介绍

文件操作库函数的简单使用

C语言中的文件操作函数如下:
文件操作函数****功能fopen打开文件fclose关闭文件fputc
写入一个字符
fgetc读取一个字符fputs写入一个字符串fgets读取一个字符串fprintf
格式化写入数据
fcanf格式化读取数据fwrite向二进制文件写入数据fread从二进制文件读取数据fseek设置文件指针的位置ftell计算当前文件指针相对于起始位置的偏移量rewind设置文件指针到文件的起始位置ferror判断文件操作过程中是否发生错误feof判断文件指针是否读取到文件末尾
先看一下C语言的两个库函数

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );// 写文件
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );// 读文件
  • fread: 第一个参数是从读取数据放到这,第二个参数是一次读入多少个字节的大小的数据,第三个参数是最多读几次,第四个参数是从这个流读数据。返回值是代表这次实际读的次数。只适用于文件流
  • fwrite: 第一个参数是从buffer获得数据,第二个参数是一次写入多少个字节的大小的数据,第三个参数是最多写几次,第四个参数是写数据到这个流。返回值是代表这次实际写的次数。只适用于文件流。

对文件进行写入操作示例:

#include<stdio.h>
#include<string.h>
int main()
{
  FILE* fp=fopen("log.txt","w");
  if(fp==NULL)
  {
    perror("open file fail!\n");
    exit(-1);
  }
   
  const char* msg="hello world!\n";
  int cnt=5;
  while(cnt--)
  {
    fwrite(msg,strlen(msg),l,fp);
  }
  fclose(fp);
  return 0;
}

运行结果:

对文件进行读取操作示例

#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = fopen("log.txt", "r");
    
    if (fp == NULL){
        perror("open file fail");
        return 1;
    }
    
    char buf[256] = {0};
    int ret = 0;
    while ((ret = fread(buf, 1, 13, fp))){
        printf("%s", buf);
    }
    fclose(fp);
    return 0;
}

**运行结果 **

stdin&stdout&stderr

Linux下一切皆文件,硬件设备也是被当做文件看待的,也就是说这些硬件设备也是可以通过IO打开的,并且进行读写。那他们是怎么操作的?一般来说,C语言程序运行起来,都会默认打开3个流,分别是:

  • stdin:标准输入流(键盘)
  • **stdout:**标准输出流(显示器)
  • stderr:标准错误流(显示器)

查看man手册我们就可以发现,stdin,stdout以及stderr这三个家伙实际上FILE*类型的。

extern FILE* stdin;
extern FILE* stdout;
extern FILE* stderr;

我们不需要考虑要打开键盘和屏幕这些流。这也是为什么我在打印数据到屏幕或从键盘上输入数据时,即使我们没有打开这些流,我们也可以执行这些操作的原因。

将文件操作中写文件的库函数传参进行更改,我们选择不传文件,而是传一个stdout,因为它的类型是FILE*,因此我们也可以把数据写到屏幕上。

#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = fopen("log.txt", "w");
    
    if (fp == NULL){
        perror("open file fail");
        return -1;
    }
    
    const char* msg = "hello world!\n";
    int count = 5;
    while (count--){
        fwrite(msg, strlen(msg), 1, stdout);
    }
    fclose(fp);
    return 0;
}

运行结果如下所示:

** 总结:**不止C语言当中有标准输入流,标准输出流和标准错误流,C++当中也有对应的cin,cout和cerr,其他语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。

系统文件I/O

操作系统除了C语言接口,C++接口或是其他语言接口外,操作系统也有一套系统接口来进行文件的访问。

相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。

我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。

系统调用接口的介绍

open

**作用:**打开一个文件

函数原型:

int open(const char* pathname,int flags);
int open(const char* pathname,int flags,mode_t mode);

参数介绍:

  • **pathname:**要打开或创建的目标文件的路径名
  • **flags:**打开文件时,可以传入多个参数,用传入的参数进行或运算,得出flags
  • **O_RDONLY:**只读打开
  • O_WRONLY:只写打开
  • O_RDWR:读,写打开这三个常量,必须指定一个且只能指定一个
  • **O_CREAT:**若文件存在,则创建它。需要使用mode选项,来指明新文件的访问权限。
  • O_APPEND:追加写

说明:这里的每一个选项都只有一个比特位是为1的,其余都是0,所以将这些选项组合就是对这些选项进行或运算,然后传入flags.

  • mode:文件权限。在新文件被创建时,参数mode具体指明了使用权限。他通常也会被umask修改。所以一般新建文件的权限为(mode&~umask).

返回值:

  • 成功:返回新的文件描述符
  • 失败:-1

**实例演示:**open函数的使用,研究函数的返回值

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd1 = open("log.txt1", O_RDONLY|O_CREAT, 0664);
    int fd2 = open("log.txt2", O_RDONLY|O_CREAT, 0664);
    int fd3 = open("log.txt3", O_RDONLY|O_CREAT, 0664);
    int fd4 = open("log.txt4", O_RDONLY|O_CREAT, 0664);
    int fd5 = open("log.txt5", O_RDONLY|O_CREAT, 0664);
    
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    printf("fd5:%d\n", fd5);
    
    return 0;
}

运行结果如下:

观察运行结果,返回值fd是从3开始分配,且是递增的,大家看到这一串数字很容易想到数组下标。但0,1,2跑哪去了?其实在Linux下,进程会默认把3个文件描述符分配(0,1和2)给标准输入,标准输出和标准错误,所以,后序如果打开文件,文件描述符就是从3开始分配的。

close

**作用:**关闭文件

函数原型:

int close(int fd);

使用close函数传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

write

函数原型:

ssize_t write(int fildes,const void* buf,size_t nbyte);

** 函数参数:**

  • fd:在文件描述符为fd的文件中进行写入
  • **buf:**从buf位置开始读取数据
  • nbyte:从buf位置开始读取nbyte个字节到文件中

函数返回值:

  • 成功:返回实际写入数据的字节数
  • 失败:返回-1

实例演示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0664);

  char buf[15] = "hello world\n";
  write(fd, buf, sizeof(buf)/sizeof(buf[0]));

  close(fd);
  return 0;
}

**代码运行结果如下: **

read

作用:读文件

函数原型:

ssize_t read(int fd,void* buf,size_t count);

** 函数参数:**

  • ** fd**:在文件描述符为fd的文件中开始读
  • **buf:**把读得内容从buf的位置开始存放
  • **count:**从buf位置开始存放count个字节

** 函数返回值:**

  • **成功:**返回实际读取数据的字节数
  • **失败;**返回-1

实例演示:f

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0){
        perror("open");
        return 1;
    }
    char ch;
    while (1){
        ssize_t s = read(fd, &ch, 1);
        if (s <= 0){
            break;
        }
        write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
    }
    close(fd);
    return 0;
}

运行结果如下所示:

文件描述符fd

**fd:**打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定读写的文件。

文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。

因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表得到形式链接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。

而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。

进程和文件之间的对应关系是如何建立的?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct,mm_struct,页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。

而task_struct当中有一个指针,该指针指向一个名为file_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。

当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。

因此,我们只要有一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。

**注意:**向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。

什么叫做进程创建的时候会默认打开0,1,2?

0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。

而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘,显示器,显示器形成各自的struct_file,将3个struct_file连入文件双链表中,并将这3个struct_file的地址分别填入fd_array数组下标为0,1,2的位置,至此就默认打开了标准输入流,标准输出流和标准错误流。

磁盘文件VS内存文件?

当文件存储在磁盘当中时,我们将其称为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称为内存文件。

磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。

文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取,输入或输出等操作时,再延后式的加载文件数据。

文件描述符的分配规则

在前面已经演示了fd的分配规则,就是默认从3开始分配,因为Linux进程默认情况下会打开3个缺省的文件描述符,上面介绍过。所以我们的进程在打开文件时,就是从3开始分配。下面做一个实验:关闭fd为0的文件,也就是标准输入,此时我们打开两个文件,看看这两个fd分别是多少

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    close(0);
    int fd1 = open("log.txt1", O_WRONLY|O_CREAT, 0664);
    int fd2 = open("log.txt2", O_WRONLY|O_CREAT, 0664);
    
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);

    close(fd1);
    close(fd2)    
    return 0;
}

运行结果:

观察结果,可以发现,关闭fd为0的文件后,后序打开的文件,文件描述符就是从0开始分配,然后分配没有被使用的fd,也就是3。从最小被使用的文件描述符开始分配。

文件描述符分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,做为新的文件描述符。

重定向

概念

概念:重定向是指修改原来默认的一些东西,对原来系统命令的默认执行方式进行改变。

** 重定向一般有以下几种方式:**

  • 输出重定向:>
  • 输入重定向:<
  • 追加重定向:>>

演示:

正常使用echo命令,字符串是输出在显示器上,加了输出重定向后,字符串被输出到文件上,也就是把本应该打印到显示器上的内容打印到了文件上。输出重定向改变了默认的输出方式。

原理

输出重定向

输出重定向的本质就是修改文件描述符下标对应的struct file*的内容。将本应该输出到一个文件的数据重定向输出到另一个文件中。

例如,如果我们想让本应该输出到"显示器文件"的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将"显示器文件"关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if (fd < 0){
        perror("open");
        return 1;
    }
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    fflush(stdout);    
    close(fd);
    return 0;
}

运行结果如下,可以看到显示器上没有输出数据,对应数据输出到了log.txt文件当中

注意:

  • printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
  • C语言中的数据并不是立刻可以写到内存操作系统里面,而是写到C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

追加重定向

**实例:**关闭标准输出流,也就是fd为1的文件,此时我们再以追加(O_APPEND)的方式打开一个文件,然后进行输出,观察现象。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 00644);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("你好啊");
    fflush(stdout);
    close(fd);

    return 0;
}

运行结果

根据输出重定向原理,我们也不难介绍这个现象,关闭了标准输出流,以追加的方式打开一个文件,这个文件被分配一个为1的文件描述符。因为printf是库函数,是往fd为1的文件进行输出,所以这里也是直接在log.txt文末进行追加

输入重定向

实例:如果我们想让本应该从"键盘文件"读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将"键盘文件"关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0

代码;

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    close(0);
    int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
    if (fd < 0){
        perror("open");
        return 1;
    }
    char str[40];
    while (scanf("%s", str) != EOF){
        printf("%s\n", str);
    }
    close(fd);
    return 0;
}

运行结果:

dup2系统调用

作用:复制文件描述符给一个新的文件描述符,让fd_array数组中下标为oldfd的内容拷贝给下标为newfd的内容,也就是让newfd的指向发生改变,指向oldfd所指向的文件。

函数原型:

int dup2(int oldfd,int newfd);

参数介绍:

  • **oldfd:**要复制的文件的文件描述符
  • **newfd:**让文件描述符文newfd的文件称为文件描述符oldfd文件的一份拷贝

函数返回值:调用成功,返回newfd,否则返回-1

实例演示:例如,我们将打开log.txt时获取到对的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件中。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
    int fd = open("log.txt",O_WRONLY|O_TRUNC);
    if (fd < 0){
        perror("open");
        return 1;
    }
    dup2(fd, 1);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n",stdout);
    close(fd);
    return 0;
}

运行结果:printf是C库中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入。

FILE

**概念:**FILE是C语言的一个对文件进行描述的一个结构体。因为IO相关的函数与系统调用接口是对应的,且库函数封装了系统调用,所以本质上访问文件都是通过fd进行访问的,所以C语言中的FILE结构体内部,必定封装了fd.

首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct_IO_FILE结构体的一个别名。

typedef struct _IO_FILE FILE;

而我们在/usr/include/libio.h头文件中可以找到struct_IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。

struct _IO_FILE {
    int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

    //缓冲区相关
    /* The following pointers correspond to the C++ streambuf protocol. */
    /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    char* _IO_read_ptr;   /* Current read pointer */
    char* _IO_read_end;   /* End of get area. */
    char* _IO_read_base;  /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr;  /* Current put pointer. */
    char* _IO_write_end;  /* End of put area. */
    char* _IO_buf_base;   /* Start of reserve area. */
    char* _IO_buf_end;    /* End of reserve area. */
    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base;  /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */

    struct _IO_marker *_markers;

    struct _IO_FILE *_chain;

    int _fileno; //封装的文件描述符
#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
    /* 1+column number of pbase(); 0 is unknown. */
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];

    /*  char* _save_gptr;  char* _save_egptr; */

    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

从FILE结构体的源码上看,FILE内部其实是封装了fd的,这里面就是_fileno,里面还有对缓冲区的划分,这里的缓冲区就是C语言级别的缓冲区,之前进度条小程序试验过。

** C语言中的fopen函数究竟是做什么?**

fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。

而C语言中的其他文件操作函数,比如fread,fwrite,fputs,fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行一系列操作。

FILE当中的缓冲区

我们来看看下面这段代码,代码中分别用了两个C库函数和一个系统接口向显示器输出内容,在代码最后还调用了fork函数。

#include <stdio.h>
#include <unistd.h>
int main()
{
    //c
    printf("hello printf\n");
    fputs("hello fputs\n", stdout);
    //system
    write(1, "hello write\n", 12);
    fork();
    return 0;
}

运行结果如下;

将程序的结果重定向到log.txt文件当中后, 我们发现文件当中的内容与我们直接打印输出到显示器的内容不一样

那为什么C库函数打印的内容重定向到文件后变成了两份,而系统接口打印的内容还是原来的一份?

首先应该知道,缓冲的方式有以下三种:

  • 无缓冲
  • 行缓冲(常见的对显示器进行刷新数据)
  • 全缓冲(常见的对磁盘文件写入数据)

当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。

而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程,一份子进程,所以重定向到log.txt文件当中printf和puts函数打印的数据就有两份。但由于write函数时系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。

** 这个缓冲区是谁提供的?**

实际上这个缓冲区是C语言自带的,如果说这个缓冲区是操作系统提供的,那么printt,fputs和write函数打印的数据重定向到文件后都应该打印两次

这个缓冲区在哪?

我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关信息的。

//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr;   /* Current read pointer */
char* _IO_read_end;   /* End of get area. */
char* _IO_read_base;  /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr;  /* Current put pointer. */
char* _IO_write_end;  /* End of put area. */
char* _IO_buf_base;   /* Start of reserve area. */
char* _IO_buf_end;    /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base;  /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

也就是说,这里的缓冲区是由C语言提供的,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。

操作系统有缓冲区吗?

操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或显示器上。 因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。

总结:printf和fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,数据也并不是直接刷新在硬件设备上,而是先刷新到操作系统的缓冲区,最后由操作系统来刷新到对应的硬件设备上。

文件系统

我们知道文件可以分为磁盘文件和内存文件,内存文件前面已经说过,现在说说磁盘文件

初始inode

概念:inode是在Linux操作系统中的一种数据结构,其本质是结构体,它包含了与文件系统中各个文件相关的一些重要信息。在Linux中创建文件系统时,同时回创建大量的inode。

磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小以及文件创建时间等信息都是文件属性,问问价属性又被称为元信息。

在命令行输入ls -l,即可显示当前目录下各文件的属性信息。

其中各列信息所对应的信息如下所示:

在LINUX操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了编号

在命令行输入ls -i即可显示各个文件的inode编号

无论是文件内容还是文件属性,都是存储在磁盘当中的。

磁盘的概念

磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备,与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。

磁盘在冯诺依曼体系结构中既可以充当输入设备,又可以充当输出设备

磁盘的寻优方案

对磁盘进行读写操作时,一般有以下几个步骤:

1.确定读写信息在磁盘的哪个盘面

2.确定读写信息在磁盘的哪个柱面

3.确定读写信息在磁盘的哪个扇区

通过以上步骤,可以确定信息在磁盘中的读写位置。

磁盘的分区与格式化介绍

** 线性存储介质**

理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。

磁盘分区

磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可以被分为十亿多个扇区。

计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分, 盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件性质分的越细,按照更为细节的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。

在Linux操作系统下,我们也可以通过以下命令查看我们磁盘的分区信息。

**磁盘格式化 **

当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作1,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。

简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。

其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2,EXT3,XFS等。

**EXT2文件系统的存储方案 **

计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group).

注意:启动块的大小是确定的,而快组的大小是油格式化确实的,并且不能更改。

其次,每个组块都有着相同的组成结构,每个组块都由超级快(Super Block),快组描述符表(Group Descriptor Table),块位图(Block Bitmap),inode位图(inode Bitmap),inode表(inode Table)以及数据表(Data Block)组成。

  • Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。
  • Block Bitmap: 块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  • inode Bitmap: inode位图当中记录着每个inode是否空闲可用。
  • inode Table: 存放文件属性,即每个文件的inode。
  • Data Blocks: 存放文件内容。

注意:

如何理解创建一个空文件?

  • 通过遍历inode位图的方式,找到一个空闲的inode
  • 在inode表中找到对应的inode,并将文件的属性信息填充进inode结构中
  • 将该文件的文件名和inode指针添加到目录文件的数据块中。

如何理解对文件写入信息?

  • 通过文件的inode编号找到对应的inode结构。
  • 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块
  • 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。

如何理解删除一个文件?

  • 将该文件对应的inode在inode位图当中置为无效。
  • 将该文件申请过的数据块在块位图当中置为无效。

因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。

为什么拷贝文件的时候特别慢,而删除文件却特别块?

因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效,无需真正的删除文件,所以拷贝慢,而删除文件时很快的。

** 如何理解目录?**

  • 都说在Linux下一切皆文件,目录当然也可以被看作为文件.
  • 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小,目录的拥有者等。
  • 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。

注意:每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。

软硬链接

软链接

概念:软链接通过名字引用另一个文件,具有独立的inode,是一个独立的文件。里面的数据块存储了源文件的路径信息。

通过以下命令创建了一个文件的软链接

ln -s myfile myfile_S

通过ls -i -l命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小要比源文件的大小小得多

软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件, 该文件有自己的inode号,但是该文件只包含源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。

但是软链接只是源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名, 但缺不能执行或查看软链接的内容了

硬链接

概念:硬链接通过inode引用另外一个文件和指向的文件共享同一个inode,不是一个独立的文件。在linux中可以让多个文件名对应于同一个inode.

In myproc myproc _c

通过ls -i -l 命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数变成了2

硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为655989的文件有两个,因此该文件的硬链接数为2。

与软链接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。

总之,硬链接就是让多个不在或者同一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。

** 为什么刚创建的目录的硬链接数是2?**

我们创建的一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?

因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就是说明它们代表的实际上是一个文件。

软硬链接的区别

  • 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode
  • 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。

本文转载自: https://blog.csdn.net/m0_58367586/article/details/126460248
版权归原作者 接受平凡 努力出众 所有, 如有侵权,请联系我们删除。

“【Linux篇】第十篇&mdash;&mdash;基础IO(系统文件IO+文件描述符+重定向+文件系统+软硬链接)”的评论:

还没有评论