我是靠谱客的博主 漂亮茉莉,最近开发中收集的这篇文章主要介绍Linux基础IO,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

      • Linux基础IO
        • C语言文件IO
          • C语言文件操作库函数
          • stdin&stdout&stderr
        • 系统文件IO
          • open
          • close
          • write
          • read
        • 文件描述符
        • 文件描述符的分配规则
        • 重定向
          • 输出重定向
          • 追加重定向
          • 输入重定向
          • dup2
        • FILE
          • FILE中的文件描述符
          • FILE中的缓冲区
        • 文件系统
          • inode
          • 磁盘的概念
          • 磁盘分区与格式化
        • 软硬链接
          • 软链接
          • 硬链接

Linux基础IO

C语言文件IO

C语言文件操作库函数

C语言文件操作的库函数有很多,在前面的文章中已有介绍,如果不太清楚的小伙伴可以去看一下作者的这篇文章:(169条消息) C语言进阶:文件操作_Ustinian%的博客-CSDN博客

这里对于C语言文件操作的库函数,作者主要来给大家复习一下C语言的文件读写函数——fwrite与fread

我们在Linux通过man指令来查看一下这两个函数:

在这里插入图片描述

可以看到这两个函数都是库函数,并且头文件都是stdio.h。

下面我先来为大家介绍一下这两个函数的参数分别是什么意思。

  • fread:第一个参数表示你要把从输入流中读到的数据放到哪里去,第二个参数表示你每次读多少个字节的数据,第三个参数表示你要多读几次,第四个参数这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流,同时也是我们读数据的地方。
  • fwrite:第一个参数表示你要往输出流里面写的数据,第二个参数表示你每次要写多少个字节的数据,第三个参数你要写几次,第四个参数这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流,同时也是我们写数据的地方。

上面介绍完了这两个函数的各个参数,接下来就来为大家展示一下这两个函数的用法吧。

写文件:fwrite

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

int main()
{
    FILE* fp = fopen("myfile","w");
    if(fp==NULL)
    {
        perror("fopen");
    }
    const char* msg = "hello worldn";
    fwrite(msg,strlen(msg),1,fp);
    
    return 0;
}

运行结果:

在这里插入图片描述

读文件:fread

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

int main()
{
    FILE* fp = fopen("./myfile","r");
    if(fp==NULL)
    {
        perror("fopenn");
    }
    const char* msg = "hello worldn";
    char buffer[64];
    ssize_t s = fread(buffer,1,strlen(msg),fp);
    if(s>0)
    {
        buffer[s-1] = 0;
        printf("%sn",buffer);
    }
    fclose(fp);
    return 0;
}

运行结果:

在这里插入图片描述

stdin&stdout&stderr

Linux下一切皆文件,也就是说在Linux下任何东西都可以被看作是文件,因此我们的键盘、显示器在Linux下同样也可以看作是文件。比如说:我们能够看到显示器上面有数据,那是因为我们向”显示器文件“写入了数据,电脑能获取到我们敲键盘时对应的字符,那是因为电脑从”键盘文件“读取了数据。

当我们程序运行起来之后,变成了进程之后,默认情况下,操作系统会帮我们进程打开三个标准输入输出!即标准输入、标准输出以及标准错误。对应到C语言上就是stdin、stdout以及stderr。其中标准输入对应的设备是键盘,标准输出和标准错误对应的设备都是显示器。

我们通过查看man手册可以发现:

#include<stdio.h>

extern FILE* stdin;
extern FILE* stout;
extern FILE* sterr;

stdin、stdout以及stderr他们三个都是FILE* 类型的指针,即我们之前说的文件指针。这也就是为什么我们在没有打开这三个流却可以往显示器打印数据,在键盘上读取数据的原因。

了解了这些之后,我们下面有一个问题:对于现在的你,输出信息到显示器,你有哪些方法呢?

  • 直接通过调用库函数printf将信息打印到显示器上面
  • 因为上面说过在C语言中标准输出对应的就是stdout,标准错误对应的是sterr,并且stdout与stderr对应的设备就是显示器因此我们还可以使用fprintf函数将信息输出到stdout与stderr上从而达到要求。
#include<stdio.h>
#include<sting.h>

int main()
{
    printf("hello worldn");
    fprintf(stdout,"hello stdoutn");
    fprintf(stderr,"hello stderrn");
    
    return 0;
}

运行结果:

在这里插入图片描述

注意: 不光C语言当中有标准输入、标准输出与标准错误,C++当中也有对应的cin、cout以及cerr,其他语言中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。

系统文件IO

操作文件,除了上述C接口(当然, C++也有接口,其他语言也有),我们还可以采用系统接口来对进行文件访问。

还记得我们直接说过的库函数和系统调用嘛,他们两个是上下级关系,库函数的底层是经过系统调用封装得到的,也就是说我们C/C++使用的文件操作的库函数底层都是通过系统调用接口进行了封装得到的。

了解了这些之后,下面我们来讲一下几个系统调用接口: open、write、read、close

open

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

我们通过man查看open,可以看到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若目标文件不存在,则创建它
    O_APPEND以追加的方式打开文件

    注意:这几个选项的二进制序列当中都只有一个比特位是1,其他比特位全为0,且为1的比特位是各不相同的,因此我们若是想将这些选项组合起来使用只需要通过或运算即可。

  • mode: 文件权限。在一个文件被创建出来后,尽管你指定了mode,但是最终的值不会是你指定的那个mode,因为文件的权限还要受到umask(文件默认掩码)的影响。比如说:你创建一个文件你将其mode设置为0666,但是这个文件由于收到了umask的影响创建出来后的权限会是0644,umask的值一般为0002,实际创建出来的文件权限为code&(~umask)。 这也就是为什么上面设置文件的mode是0666,但是创建出来权限确实0644的原因。

  • 返回值: 创建成功,返回被打开文件的文件描述符,创建失败返回-1。

下面我们就来使用一下这个系统调用接口吧

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

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

运行结果:

在这里插入图片描述

我们可以看到文件描述符fd是从3开始分配的,且它是连续的,对于一段连续的数字你能联想到什么呢?那为什么文件描述符fd是从3开始分配的,0、1、2去哪了呢?

没错,对于这些连续的数字我们可以联想到数组的下标,这些文件描述符其实就是一个数组的下标。还记得我们上面说过:当一个程序运行起来变成进程之后,默认情况下,操作系统会帮我们进程打开三个标准输入输出。文件描述符0,1,2分别给了标准输入、标准输出和标准错误。所以我们打开文件的时候,文件描述符是从3开始分配的。

close

作用: 关闭文件

close函数原型如下:

int close(int fd);

函数参数:

  • fd: 文件描述符
  • 返回值: 若关闭文件成功,返回0,关闭失败返回-1.
write

作用: 往一个文件里面写数据

函数原型如下:

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

函数参数:

  • fd: 文件描述符
  • buf: 你要往这个文件里面写的数据
  • count: 从buf开始向后count个字节的数据写到这个文件描述符所对应的文件中
  • 返回值: 如果写入成功,返回实际写入数据的字节个数,写入失败返回-1.

下面我们就来使用一下这个系统调用接口吧

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

int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd<0)
    {
        perror("openn");
        return 1;
    }
    const char* msg = "hello worldn";
    write(fd,msg,strlen(msg));
    close(fd);
    
    return 0;
}

运行结果:

在这里插入图片描述

read

作用: 从一个文件中读取信息

函数原型如下:

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

函数参数:

  • fd: 文件描述符
  • buf: 将读取到的信息放到buf中
  • count: 从这个文件描述符所对应的文件中读取count个字节的数据
  • 返回值: 读取成功,返回实际读取数据的字节个数,读取失败返回-1。

下面我们就来使用一下这个系统调用接口吧

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

int main()
{
    int fd = open("log.txt",O_RDONLY | O_CREAT,0666);
    if(fd<0)
    {
        perror("openn");
        return 1;
    }
    const char* msg = "hello worldn";
    char* buf[64];
    ssize_t s = read(fd,buf,strlen(msg));
    if(s>0)
    {
        buf[s-1] = 0;
        printf("%sn",buf);
    }
    close(fd);
    
    return 0;
}

运行结果:

在这里插入图片描述

文件描述符

操作系统中有着许多的进程,文件是在进程运行的时候打开的,一个进程是可以打开多个文件的。我们知道在操作系统中进程多了,我们就需要将它给管理起来。在系统中会存在着大量被进程打开的文件,那我们需不需要将他们也给管理起来呢?

答案是要的。

那么问题又来了,我们如何管理呢?

六字真言:先描述,再组织。

操作系统会为这些已经被打开的文件分别创建一个struct file的结构体:

在这里插入图片描述

因此对于这些文件的管理,就变成了对这些结构体的管理了。我们再将这些结构体以双链表的形式连接起来,之后操作系统对于文件的管理就变成了对链表的增删改查。

上面你说的文件要被管理起来我明白了,但是现在我又有一个问题了:一个进程是可以打开多个文件的,因此它们之间的关系就是1:n,那这么多的文件我怎么知道哪些是我们进程的呢?

在前面学习进程的时候我们知道,当一个程序运行起来,它的代码和数据被加载到内存,操作系统会为其创建tast_struct、mm_struct、页表等一系列的数据结构,并通过页表建立虚拟地址空间和物理内存的映射关系,然后这个程序就变成了一个进程。

在这里插入图片描述

今天我们还要再来说一下进程控制块task_struct,每个进程的task_struct中都有一个struct files_struct*的结构体指针,操作系统为了让进程和该进程打开的文件关联起来,在内核创建了一个struct file_struct的结构体,其中这个struct files_struct结构体中又包含了一个名为fd_array的结构体指针数组,该数组的下标就是我们的文件描述符,数组的内容就是我们所打开文件的地址。如下图所示:

在这里插入图片描述

现在我们就知道,文件描述符是从0开始的数组下标。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于就是有了struct file结构体,表示一个已经打开的文件对象。当我们的进程执行read、write、open等系统调用时,为了让进程和文件关联起来,每个进程的task_struct都有一个files_struct*的指针,指向file_struct结构体,该结构体中最重要的部分就是包含一个指针数组,该指针数组每个元素都是一个指向已经打开文件的指针!因此每当我们进程执行系统调用打开一个文件时,就会将该文件的地址填到该指针数组中,然后对应一个文件描述符。

文件描述符的本质时内核中进程和文件产生关联的数组的下标!!!

因此,只要我们有该文件的文件描述符,我们就可以找到对应的文件,进而对该文件进行一些列操作。

文件描述符的分配规则

我们来做一个实验:我们连续打开五个文件,看看这五个文件分配的文件描述符分别是多少。

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

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

运行结果:

在这里插入图片描述

可以看到打开的这几个文件的文件描述符是一串连续的数字,且从3开始。这和我们的预期一样,因为上面说过创建一个进程时,会默认打开标准输入、标准输出、标准错误,fd_array给他们三个分配的文件描述符分别是0、1、2.因此我们后面创建文件的时候分配的描述符会从3开始。

下面我们再来做一个实验:既然你说创建一个进程后,fd_array会把0、1、2这三个文件描述符分配给标准输入、标准输出和错误。那么我现在有一个问题,假如我现在把标准输入0给关闭了,然后我再创建一个文件,这个时候给该文件分配的文件描述符会是多少呢?

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

int main()
{
    close(0);
    int fd = open("log.txt",O_RDONLY | O_CREAT,0666);
    
    printf("fd: %dn",fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到当我们把标准输入0给关闭后,我们去创建一个文件,分配给这个文件的描述符是0.

下面我们再来做最后一个实验:假如我现在把标准错误2给关闭了,然后我再创建一个文件,这个时候给该文件分配的文件描述符会是多少呢?

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

int main()
{
    close(2);
    int fd = open("log.txt",O_RDONLY | O_CREAT,0666);
    
    printf("fd: %dn",fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到当我们把标准错误2给关闭后,我们去创建一个文件,分配给这个文件的描述符是2.

结论: 通过上面做的这几个实验我们就可以知道,文件描述符的分配规则是:给新创建文件分配的文件描述符(fd),是从fd_array中找一个最小的、未被使用的,作为这个文件的文件描述符(fd)。

重定向

在上面介绍了文件描述符以及文件描述符的规则后,我们就有了足够的知识储备来理解重定向。

常见的重定向一般有以下三种:

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

下面我们就依次来讲这三种重定向的原理

输出重定向

不知道大家是否还记得我们以前敲的这段代码:

在这里插入图片描述

以前只和大家说过这个就是输出重定向,但我们那个时候其实对于输出重定向并不太清楚,也根本讲不清清楚,只是有个简单的印象。接下来我就来和大家说一下输出重定向的原理。

原理:输出重定向的原理就是本来应该输出到“显示器”文件里面的内容输出到了另外一个文件中。

在这里插入图片描述

知道了原理之后下面我们再来看一下输出重定向的代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.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("fd: %dn".fd);
    printf("hello worldn");
    printf("hello mlfn");
    printf("hello worldn");
    
    fflush(stdout);
    close(fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到我们运行这个可执行程序的时候,屏幕上面并没有打印我们输出的内容,反而在我们查看log.txt文件的时候,发现我们输出的内容全打到了这里面。

注意: printf函数默认是向stdout标准输出中输出数据的,标准输出流stdout其实指向的是一个struct FILE类型的结构体,该结构体中有一个存储文件描述符的变量,stdout它指向的这个结构体中存储的文件描述符就是1,而1这个文件描述符一般是指向标准输出的。因此printf它实际就是向文件描述符为1的文件输出数据,这也就是为什么当1号文件描述符分配给标准输出的时候,我们printf输出的数据是打印到显示器上,当1号文件描述符分配给log.txt时,我们printf输出的数据就是打印到了文件中。

追加重定向

不知道大家是否还记得我们以前敲的这段代码:

在这里插入图片描述

这个就是我们以前提到过的追加重定向。

下面我就来给大家讲一下追加重定向的原理

原理:输出重定向和追加重定向的原理基本类似,唯一的区别追加重定向在输出重定向的基础上还要加上以追加的方式打开该文件。如果说输出重定向是覆盖式的输出数据,那么追加重定向就是追加式的输出数据。

知道了追加重定向的原理之后,下面我们来看一下追加重定向的代码:

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

int main()
{
    close(1);
    int fd = open("log.txt",O_WRONLY | O_APPEND | O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %dn".fd);
    printf("hello worldn");
    printf("hello worldn");
    printf("hello worldn");
    printf("hello worldn");
    
    fflush(stdout);
    close(fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到我们printf打印的数据就以追加的方式输出到了log.txt文件中。

输入重定向

下面我们来看一下我们以前敲的一段输入重定向的代码:

在这里插入图片描述

以前只和大家说过这个就是输入重定向,但我们那个时候其实对于输入重定向并不太清楚,也根本讲不清清楚,只是有个简单的印象。接下来我就来和大家说一下输入重定向的原理。

原理:本来应该从“标准输入”中读取数据,现在重定向到从另一个文件中读取数据。

在这里插入图片描述

知道了输入重定向的原理之后,下面我们来看一下输入重定向的代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.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 buffer[64];
    while(scanf("%s",buffer) != EOF)
    {
        printf("%sn",buffer);
    }
    close(fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到我们我们本来是应该从标准输入(键盘)去读取数据,现在经过重定向之后,我们从log.txt中读取数据了。

注意:scanf函数默认是向stdin标准输入中读取数据的,准输入流stdin其实指向的是一个struct FILE类型的结构体,该结构体中有一个存储文件描述符的变量,stdin它指向的这个结构体中存储的文件描述符就是0,而0这个文件描述符一般是指向标准输入的。因此scanf它实际就是从文件描述符为0的文件中读取数据,这也就是为什么当0号文件描述符分配给标准输入的时候,我们scanf是从键盘上读取数据,当0号文件描述符分配给log.txt时,我们就从log.txt中读取数据。

dup2

我们要完成上面三个重定向,我们好像都必须得先关闭一个文件描述符。如果每次完成一个重定向,我们都得先关闭一个文件描述符,这样是不是太麻烦了。于是我现在就有了一个问题:我们可以在不关闭文件描述符的前提下,完成重定向嘛?

答案是可以的。

首先我们需要明白:输入输出追加重定向的本质其实就是将其对应的文件描述符分配给我们新打开的文件,从而达到重定向。那么我们想要在不关闭文件描述符的前提下完成重定向,只需要让文件描述符表中对应的文件描述符(数组下标)的内容指向我们新打开的文件即可。

下面我们通过图片来为大家分析一下系统调用接口dup2的原理:

在这里插入图片描述

下面我们先通过man来查看一下dup2这个系统调用接口

在这里插入图片描述

作用: 将新的文件描述符变成就旧的文件描述符的一份拷贝,即将fd_array数组中fd_array[oldfd]的内容拷到fd_array[newfd]中,其实也就是让newfd指向oldfd所指向的文件。如果在重定向前,如果newfd已分配给了某个文件,我们还需要先关闭newfd所指向的文件。

dup2的函数原型如下:

int dup2(int oldfd,int newfd);

注意:

  • 如果oldfd不是一个有效的文件描述符,则dup2调用失败,那么此时newfd所指向的文件就不会被关闭
  • 如果oldfd是一个有效的文件描述符,并且newfd和oldfd的值是一样的,则dup2不做任何操作,返回newfd。

下面我们就是用dup2来完成一下输出重定向吧

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

int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    
    dup2(fd,1);//oldfd,newfd
    printf("fd: %dn",fd);
    printf("hello worldn");
    printf("hello Linuxn");
    printf("hello mlfn");
    
    fflush(stdout);
    close(fd);
    return 0;
}

运行结果:

在这里插入图片描述

可以看到我们使用dup2完成了输出重定向,分配给log.txt的文件描述符是3,并且我们并没有关闭1号文件描述符。

FILE

FILE中的文件描述符

我们前面说过,库函数与系统调用之间是上下层关系,我们的库函数是通过系统调用封装得来的,所以本质上,访问文件都是通过文件描述符fd来进行访问的,所以我们可以大胆的猜测:C库当中FILE结构体内部,必定封装了fd。

我们可以在/usr/include/stdio.h头文件中看到下面这段代码

typedef struct _IO_FILE FILE;

我们可以看到FILE其实就是struct _IO_FILE结构体的一个别名,那么这个时候可能会有人问了:那这个struct _IO_FILE结构体里面有什么呢?

下面我们进入/usr/include/libio.h这个头文件中查看一下struct _IO_FILE这个结构体的定义:

在这里插入图片描述

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
};

我们在struct _IO_FILE结构体中的成员当中,可以看到一个名为 _fileno的成员,这个成员其实就是封装的文件描述符,到这里就验证了我们上面的猜测:C库当中FILE结构体内部,必定封装了fd。

知道了上面的这些之后,下面我们再来理解一下C语言当中fopen函数究竟干了什么?

fopen函数在上层为用户申请FILE结构体,并返回该结构体的地址(FILE*),在底层通过系统调用接口open打开或者创建对应的文件,为其分配一个文件描述符fd,并将文件描述符fd填入到FILE结构体中的_fileno变量中,如以此来便完成了文件的打开操作。

FILE中的缓冲区

我们下面先来看一段代码:

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

int main()
{
    const char* str = "hello writen";
    const char* msg = "hello printfn";
    const char* ptr = "hello fprintfn";
    
    write(1,str,strlen(str));
    printf("%s",msg);
    fprintf(stdout,"%s",ptr);
    
    fork();
    
    return 0;
}

运行结果:

在这里插入图片描述

可以看到write、printf、fprintf函数都将其对应的内容输出到了显示器上。

下面我们在来看一段代码:

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

int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    const char* str = "hello writen";
    const char* msg = "hello printfn";
    const char* ptr = "hello fprintfn";
    
    write(1,str,strlen(str));
    printf("%s",msg);
    fprintf(stdout,"%s",ptr);
    
    fork();
    fflush(stdout);
    close(fd);
    return 0;
}

运行结果:

在这里插入图片描述

咦,我们发现经过输出重定向之后,printf和fprintf都输出了两次,而write只输出了一次。

下面我就有了一个问题: 上面的代码为什么经过输出重定向后,库函数的内容重定向到文件中都分别打印了两次,而系统调用接口却只打印了一次呢?

不要着急,再回答你上面的问题之前,我先来讲一下缓冲区的刷新策略。

缓冲区有以下三种刷新策略:

  • 立即刷新(不缓冲)
  • 行刷新(行缓冲n),比如:显示器打印
  • 缓冲区满了,才刷新(全缓冲),比如:往磁盘文件中写入数据。

知道了缓冲区的刷新策略之后,我们得明白一件事情,这个缓冲区一定不是操作系统提供的。

你为什么说它一定不是操作系统提供的呢?那如果不是操作系统提供的,那是谁提供的呢?

因为如果他是操作系统所提供的缓冲区,那么经过输出重定向后,write、printf与fprintf函数打印的数据都应该在文件中打印两次,但是他们并不是都打了两次,所以我们可以肯定这个缓冲区一定不是操作系统所提供的。这里的缓冲区是由C语言所提供的。

那么问题又来了:操作系统有缓冲区吗?

操作系统其实也是有缓冲区的。我们需要明白,当我们刷新用户缓冲区的数据时,并不是直接就将用户的缓冲区就刷新到了磁盘或者显示器上,而是先将用户缓冲区上面的内容刷新到操作系统的缓冲区,最后再由操作系统将数据刷新到磁盘或者某种外设。(操作系统有自己的刷新机制,我们并不需要去关系操作系统的刷新机制)

在这里插入图片描述

了解了这些背景知识以后,下面我们就来回答上面的问题: 为什么上面的代码为什么经过输出重定向后,库函数的内容重定向到文件中都分别打印了两次,而系统调用接口却只打印了一次呢?

当我们没有进行输出重定向,然后执行可执行程序时,最终会将数据打印到显示上,此时采用的是行缓冲,因为每个字符串的后面都带有一个n,所以当我们执行完write、printf以及fprintf函数就会立即将数据打印到显示器上。

而当我们进行输出重定向,再执行可执行程序的时候,此时的刷新策略会由行缓冲变成全缓冲。对于全缓冲而言,它并不会将数据立即刷新,而是会等到缓冲区满了才进行刷新。因此我们使用printf与fprintf函数会将打印的数据都打印到C语言的缓冲区里,因为这些数据还并未刷新到磁盘或者显示器上面,所以这些数据还是父进程里的数据。当我们fork之后创建子进程,刚开始父子进程是共享这些数据的,但是后面当父进程或者子进程要刷新缓冲区的内容时,其本质就是对父子进程共享的数据进行了修改,因为进程之间是具有独立性的,所以这个时候就会发生写时拷贝,因此缓冲区里面的数据就由一份变成了两份。一份是子进程的,一份是父进程的,因此重定向到log.txt文件中,printf和fprinf函数打印的数据会有两份。但是对于系统调用write而言,它是没有缓冲区的,所以write函数打印的数据就只打印了一次。

文件系统

inode

如果一个文件,没有被打开,这个文件在哪里呢?磁盘。如果创建一个空文件,这个文件要不要占磁盘空间?必须的!这是因为文件有属性,属性也是数据。

因此磁盘里面的文件由两部分构成:文件内容和文件属性。文件的内容就是该文件中存储的数据,文件的属性就是文件的一些基本信息,比如说:文件大小、文件的权限、文件的类型以及文件的创建时间等等。文件属性又被称为元信息。

我们可以通过ls -l来查看当前目录下各个文件的属性信息:

在这里插入图片描述

磁盘里面的文件是由两部分组成:文件内容和文件属性。在Linux下保存元信息的结构被称为inode,文件的元信息和内容是分开存储的。因为系统中可能会存在大量的文件,而一个文件又存在着许多的信息,inode是保存这些属性信息的集合。所以为了区分各个文件的inode,我们为每个inode设置了inode编号。

我们可以通过ls -li来查看当前目录下各个文件的inode编号:

在这里插入图片描述

注意: 不管是文件的内容还是文件的属性,它们都是存储在磁盘中的。

磁盘的概念

磁盘是我们计算机中的一个机械设备(例如:SSD,FLASH卡,usb),它是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。

知道了磁盘的概念之后,下面我们来看一下磁盘的内部结构是怎么样的:

在这里插入图片描述

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

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

经过以上的三个步骤,我们就可以确定信息在磁盘中的读写位置。

磁盘分区与格式化

为了便于理解,我们可不可以将一个盘片想象成一个线性的结构呢?

答案是可以的,如果大家想象不出来,可以想一下磁带,当磁带卷起来的时候,它就像磁盘一样是圆形的。当我们把磁带给拉出来的时候它就是线性的。

在这里插入图片描述

磁盘分区: 磁盘写入的基本单位是扇区,一个扇区的大小通常是512字节。因此如果以大小为1024G的磁盘为例,该磁盘就会被分为若干个扇区。站在操作系统的角度,我们认为磁盘是线性结构的。

在这里插入图片描述

因为磁盘它是很大的,管理成本比较高。因此计算机为了更好的管理磁盘,便对磁盘进行了分区。这就好比我们的国家的领土:我们的国家是很大的,因此为了便于管理又将我们的国家划分成了30多个省。

格式化: 但是光有分区还是不够的,我们还需要对磁盘进行格式化,磁盘格式化就是对磁盘中的分区进行初始化的一种操作。简单来说,磁盘格式化就是对分区后各个区域写入对应的管理信息。这也就好比仅仅将我们国家划分成30多个省这是不够的,国家还会派一些领导班子去管理这些省份。

在这里插入图片描述

所以操作系统对磁盘的管理就是对这些分区的管理,理论上我们只要管理好一个分区,我们就可以以相同的方式管理好其他的分区。

下图是Linux ex2文件系统下磁盘文件系统图,下图只是磁盘文件的一个分区图。磁盘是典型的块设备,因此磁盘分区又会被划分成一个个的块组(Block group)因此我们现在对于磁盘的管理就变成了对这些块组的管理,我们只需要一个块组,,我们就可以以相同的方式管理好其他的块组。

在这里插入图片描述

注意: 上图中启动块(Boot Block)它的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。

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

一个文件的inode与 Data block之间的关系:

在这里插入图片描述

因此我们只要知道一个文件的inode,那我们就可以拿到该文件的属性信息以及该文件的内容。如此一来便实现了将数据和属性分开存储。

下面我们来看一张简图来帮我们更好的理解inode与Data Block的关系

在这里插入图片描述

知道了上面这些之后,下面我们来分析几个问题:

  1. 如何理解创建一个文件?

    首先我们需要先确定分区和块组,在对应的分区和块组中,遍历inode位图,找到一个未被使用的inode,然后通过inode位图与inode Table的映射关系找到对应的inode,并将文件的属性信息填入到inode结构体中。当我们要对文件进行写入操作时,首先我们需要遍历Block位图,找到若干个未被使用的数据块,然后通过Block位图与Data Blocks的映射关系找到相应的数据块,将数据写入到这些数据块中,并将相应的数据块编号填入inode的文件数据块列表中,建立inode和数据块之间的映射关系。最后还需要把inode id和文件名的映射关系添加到目录的存储列表中。

  2. 如何理解查找一个文件?

    首先我们通过文件名和inode id的映射关系找到该文件的inode,然后通过inode就可以拿到该文件的属性信息,再通过inode的数据块列表就可以找到对应的数据块,我们就可以拿到该文件的内容。

  3. 如何理解删除一个文件?

    首先我们得明白要想删除一个文件,就必须先得找到这个文件。当找到了这个文件之后我们就拿到了该文件的inode,将该文件的inode在inode位图中的那一位由1置为0,同时将该文件申请的数据块在数据块位图的那一位也由1置为0。这种删除是一种伪删除,它并没有将文件对应的信息给删掉,它只是将该文件的inode号和申请的数据块号在位图中由1置0,从而达到了删除的效果。

  4. 为什么下载文件的时候很慢,删除文件的时候很快?

    因为我们下载文件的时候首先需要创建文件,然后再将内容写入到文件中。该过程需要先申请inode号,将该文件的属性信息填入inode中,然后还需要申请数据块号,将文件的内容放到相应的数据块中,建立数据块与inode之间的映射关系,最终下载完成。而我们删除文件只需要把文件的inode号和申请的数据块号在位图中由1置0,并没有真正的删除文件,所以下载文件很慢,删除文件是很快的。

    这就好比建房子和拆房子一样:我们需要大量的人力、财力和物力并且需要花费很长的时间才能够建好一栋楼。但是拆房子的时候我们只需要在这栋楼上喷一个拆字就表示这栋楼要被拆除了。

  5. 如何理解目录?

    前面说过在Linux下一切皆文件,因此目录也是文件。既然是文件那就有它的inode和数据。目录的inode存放了它的属性信息,那目录的数据块放的是什么呢?目录的数据块存放的是文件名和inode编号的映射关系。

    注意: 每个文件的文件名并没有存放在inode中,而是存放在了文件所处目录文件的文件内容中。计算机并不关心文件的文件名,它只关心文件的inode编号,文件名是给我们用户看的。通过文件名和inode编号的映射关系我们就可以找到该文件的inode,找到了inode我们就可以拿到该文件的属性和内容,因此目录的数据块只需要存放文件名和inode编号的映射关系就可以拿到这些信息。

软硬链接

软链接

概念:软链接它有自己独立的inode,软链接是一个独立的文件,有自己的inode属性也有自己的数据块(保存的是指向文件的所在路径+文件名)

我们可以通过以下指令来创建一个文件的软链接

[root@izuf65cq8kmghsipojlfvpz 4-10]# ln -s test test_link

在这里插入图片描述

可以看到我们的软链接的inode编号与源文件的inode编号是不一样的,并且软链接的文件大小要比源文件的文件大小要小很多。

下面我们来运行一下这两个可执行程序看看会是个什么结果

在这里插入图片描述

我们可以看到这两个可执行程序运行起来之后打印的结果是一样的。但是软链接的文件大小却要比源文件的大小小很多,这个时候你能联想到什么呢?

对,没错我们可以联想到windows下的快捷方式,软链接就相当于是windows下的快捷方式。软链接它的数据块保存的是指向文件的所在路径+文件名,所以我们执行软链接的时候就相当于间接执行了这个源文件。

但是快捷方式它是不能够单独存在的,当我们的源文件被删除后,尽管有文件名,但是这个软链接就不能再执行了。

下面我们将软链接的源文件给删除掉之后再来执行一下这个软链接看看是什么结果:

在这里插入图片描述

可以看到正如我们上面所说,当我们把源文件删除之后,源文件不存在了那么你这个软链接找不到该源文件了,所以你也就运行不了了。

硬链接

概念:硬链接它不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为它没有自己的inode,我们可以将它理解成源文件的别名。

我们可以通过以下指令来创建一个文件的软链接

[root@izuf65cq8kmghsipojlfvpz 4-10]# ln test test_link

在这里插入图片描述

可以看到我们的硬链接并不是一个独立的文件,因为它的inode编号和源文件的inode编号是一样的,并且硬链接与源文件的文件大小也是一样的。那硬链接是不是就是源文件的一个别名呢?下面我们通过代码来验证一下。

下面我们来运行一下这两个可执行程序看看会是个什么结果

在这里插入图片描述

我们可以看到这两个可执行程序运行起来之后打印的结果是一样的。

下面我们将硬链接的源文件给删除掉之后再来执行一下这个硬链接看看是什么结果:

在这里插入图片描述

现在我们就可以确定了,硬链接就是源文件的一个别名,当源文件被删除后它依旧能够正常执行。

下面我们再来看一个现象:

在这里插入图片描述

现在我就有了一个问题:为什么刚刚创建的文件它的硬链接数是2呢?

这是因为每个目录创建后,该目录下还有两个隐藏文件.和…。它们分别表示当前目录和上级目录,因此我们刚刚创建的目录会有两个名字,一个是qwe一个是.所以这个目录的硬链接数就是2。

在这里插入图片描述

可以看到我们qwe目录的inode号和qwe中.的inode号是一样的,也就是说它们其实代表的是同一个文件。

总结

软硬链接的区别:

  • 软链接是一个独立的文件,它有自己的inode,硬链接是源文件的别名,它与源文件的inode是一样的。
  • 软链接它就相当于是windows下的快捷方式,必须要源文件存在的情况下才能正常运行,硬链接即使源文件被删除了它也能够正常运行。

以上就是我们这篇文章的全部内容了,如果觉得对你有帮助的话,可以三连一波支持一下作者。

最后

以上就是漂亮茉莉为你收集整理的Linux基础IO的全部内容,希望文章能够帮你解决Linux基础IO所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部