Linux | 文件系统

目录

前言

一、预备知识

二、文件相关的系统调用

1、C语言的文件操作

2、系统调用接口

(1)open函数

(2)close函数

(3)write函数

(4)read函数

3、代码实操

三、深入理解文件周边概念

1、文件描述符的理解

2、文件描述符的分配规则

3、理解重定向

四、对缓冲区的理解

1、缓冲区引入

2、缓冲区相关概念

3、C语言文件函数 VS 系统调用

4、缓冲区的刷新机制

5、再次深入理解缓冲区

6、模拟实现C语言缓冲区


前言

        文件系统是操作系统主题之一,本章就围绕着Linux下的文件系统来介绍文件系统周边知识;

一、预备知识

        首先,我打算问大家一些关于文件系统的基础知识来提升大家对文件系统的理解;

问题:假如我创建一个空文件,我不对文件做任何写入操作,如下图,此时文件会占用磁盘资源吗?;

Linux | 文件系统_第1张图片

        答案是肯定的,虽然上面大小显示0KB,但是这只是文件内容是0KB大小,那么记录这个文件大小的数据是否需要存储起来呢?文件修改时间需不需要记录下来呢?还有文件名等等;显然是肯定的,既然要记录下来,必然就会占用磁盘资源;文件 = 文件属性 + 文件内容

问题:我们对文件的操作有从大体分为哪几种呢?

        两种,有了以上结论,我们无非就是要么对文件属性操作,要么就是对文件内容操作;

问题:文件大体又可分为几种呢?

        两种,一种文件放在磁盘中,一种文件被加载进内存中,关于为什么要将文件加载进内存是由冯诺依曼体系决定的,冯诺依曼体系规定,CPU无法直接访问外设/硬件,需要借助内存,因此我必须先将在硬件里的文件加载进内存中,才可访问该文件;

问题:访问文件的本质是什么?到底是谁在访问进程?

        文件是放在硬盘上的,我们要访问文件得通过程序访问,我们写了一段访问文件的代码;编译并运行,此时操作系统会为我们生成各种内核数据结构,代码和数据也会被加载进内存中,此时在OS系统层面上,生成了一个进程,我们通过这个进程访问文件,进程又是如何访问文件的呢?要访问文件也需要将文件加载进内存中,而文件本来在硬盘中,硬盘属于外设,我们要想访问外设必须通过操作系统,而操作系统会为我们提供一层系统调用,因此我们访问文件的本质是 进程通过系统调用来访问文件;

问题:我们之前访问文件都是通过语言级函数,如C语言的文件访问接口,这里又提出使用系统调用访问文件,两者的关系是什么?我们平常又为何都是使用语言级函数而不是使用系统调用接口?

        首先阐述二者关系,实际上无论哪一种语言,都会有自己的访问文件的接口,而它们最终都只是对我们系统调用的一种封装而已,封装的本质是为了让系统调用接口的使用更简单,也是为了迎合自己语言语法特点;

        接着回答第二个问题,我们之所以平常经常看到使用语言级文件接口,而不是系统调用原因有二;其一,系统调用的难度更高,对于初级使用者很不友好,学习成本也较高;其二,也是最主要的一个原因,系统调用并不具备跨平台性,因为我们每个操作系统的系统调用接口可能会有差距,而在语言级,我们可以通过如C语言的条件编译,对多个平台的系统调用进行封装,实现跨平台性;

二、文件相关的系统调用

1、C语言的文件操作

        已经会C语言文件操作的小伙伴可略过这一部分;这一部分内容仅仅只是带着大家回顾C语言文件操作接口,方便我们后面与系统调用接口做对比,这里也不会讲的特别详细,如果想要更加详细的学习C文件操作可点击下方链接;

【精选】C语言 | 文件操作-CSDN博客

        fopen函数,打开一个文件,并返回一个FILE类型指针;其声明如下;

FILE *fopen(const char *path, const char *mode);

参数:

        第一个参数为要打开文件的路径,可以是绝对路径,也可以是相对路径;第二个参数是以什么样的方式打开,具体如下;

Linux | 文件系统_第2张图片

简单来说;

r:以读的方式打开;

r+:以读和写的方式打开;

w:以写的方式打开文件,打开之前会清空文件内容;

w+:以读和写的方式打开文件,若文件不存在会创建文件,若文件存在打开前清空文件内容;

a:以追加的形式打开文件,文件不存在会创建文件;

a+:以读和追加的形式打开文件,文件不存在则会创建文件;

        fclose函数,关闭一个文件,文件打开以后一定要记得关闭,不然可能会造成资源泄漏;这个函数直接传入一个FILE指针句柄即可;声明如下;

int fclose(FILE *fp);

        fprintf函数,往指定文件写入数据,与fwrite类似;还有读取数据函数fscanf与fread函数,这里就不做一一讲解,此处直接写入代码示例;

Linux | 文件系统_第3张图片

2、系统调用接口

        这里准备一共介绍4个系统调用,分别是open、read、write与close;这里一一逐步展开介绍;

(1)open函数

        改系统调用的主要功能是打开文件,对应C语言中的fopen;具体声明如下;

Linux | 文件系统_第4张图片

        这里可以看到我们使用前需要引入三个头文件,且有一个很奇怪的现象,这里居然有函数重载!要知道我们Linux系统调用大部分也是由C语言实现的,C语言居然由函数重载!(照顾一下基础薄弱的同学,函数重载指同名函数,不同参数列表可存在同一个作用域内;)有兴趣的同学可以去了解一下,C语言的函数重载可使用可变参数列表来实现,这里不做过多研究,也并不是本文的重点;

参数:

pathname:该参数为要打开的文件名,可为绝对路径,也可以为相对路径;

flags:标志位参数,关于这个参数略微复杂,表示该文件的打开方式,类似C语言中fopen的第二个参数,该参数定义了很多宏,每个宏有自己特定的含义,如下所示;

Linux | 文件系统_第5张图片

这里列举几个比较常用的;

O_RDONLY:只读

O_WRONLY:只写

O_RDWR:读与写

O_APPEND:追加

O_CREAT:创建

O_TRUNC:清空

        我们在使用时将一个或多个参数或在一起,比如我们想要有C语言 W+ 的功能;此时这个参数我们应该填  O_WRONLY | O_CREAT | O_TRUNC 

mode:若文件不存在,创建文件的权限;

返回值:

        该参数的返回值为一个int整型,我们称其为文件描述符,管理文件的句柄,我们通过这个句柄对打开文件进行操作,后面我们会对文件描述符进行更加深层次的理解;该函数若调用失败则返回-1,且错误码被设置;

(2)close函数

        关闭指定的文件,具体函数声明如下;

Linux | 文件系统_第6张图片

        该函数若要调用我们先引入头文件unistd.h,函数的参数也只有一个,就是管理文件的文件描述符;若该函数调用成功,则返回0,若失败,则返回-1;

(3)write函数

        向指定文件进行写入操作,具体函数声明如下;

Linux | 文件系统_第7张图片

参数:

fd:想要写入文件对应的文件描述符;

buf:我们要写入信息的缓冲区;

count:我们想要写入多少字节;

返回值:

        若该函数执行成功,则返回写入的字节数,若失败,则返回-1,错误码被设置;

(4)read函数

        向指定文件读取数据吗,声明如下;

Linux | 文件系统_第8张图片

参数:

fd:想要读取文件对应的文件描述符;

buf:将读取信息存放缓冲区的地址;

count:预计读取字节数;

返回值:

        若函数调用成功,则返回读取到的字节数;若失败,则返回-1,错误码被设置;

3、代码实操

        有了以上函数的学习,我们可以实践练习上述代码;如下所示;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 打开文件
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0660);
    if(fd == -1)
    {
        perror("open");
        exit(1);
    }
    // 定义写入字符串 
    const char* buf = "hello write\n";
    // 写入操作 问题:这里strlen需不需要+1,或者说需不需要计算\0
    ssize_t sz = write(fd, buf, strlen(buf));
    if(sz == -1)
    {
        perror("write");
        exit(2);
    }

    // 关闭文件
    close(fd);
    return 0;
}

        回答上面的问题,关于在写入时,我们在C语言通常会写入字符串结尾的\0,表示字符串结束了,这里是不需要的,因为\0是C语言层面的规定,并不是操作系统系统调用的规定,故这里不需要将\0也写入;

        我们再写一份读取数据代码,如下所示;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 打开文件
    //int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0660);
    int fd = open("log.txt", O_RDONLY, 0660);
    if(fd == -1)
    {
        perror("open");
        exit(1);
    }

    // 定义读取接收缓冲区
    char buf[128];
    // 读取文件      问题:这里计算想要读取字节个数时为什么减1
    ssize_t sz = read(fd, buf, sizeof(buf) - 1);
    if(sz == -1)
    {
        perror("read");
        exit(3);
    }
    // 加上\0
    buf[sz] = '\0';
    printf("read buf:%s\n", buf);

    // 关闭文件
    close(fd);
    return 0;
}

        回答代码提出问题,首先,程序由于是我们写的,我们很清楚,我们要读取的是字符串,我们可能并不知道要读取的字符串有多大,但是我们很清楚,我们设置的缓冲区大小只有128字节,若我们读取128字节,正好读满,那么我们的\0又放在哪里呢?所以我们最多读取127字节,故需要减去1;

补充:

         当我们使用open函数以0666的方式打开文件时,若有设置当文件不存在则创建文件,创建的文件的权限并不是0666,这是因为权限掩码在作祟,我们可以通过umask函数在代码中设置局部权限掩码;注意,该权限掩码只在本进程内生效!函数声明如下;如果不知道什么是权限掩码的可以看看我前面写的文章;

Linux | Linux权限详解-CSDN博客

Linux | 文件系统_第9张图片

三、深入理解文件周边概念

1、文件描述符的理解

        首先我们带着下面几个问题来理解文件描述符;

问题:我们前面说了,进程访问文件需要将文件加载进内存,那么计算机同一时间,可能存在多个进程,也有可能有多个进程打开访问文件,那么操作系统是如何将这些文件管理起来的呢?

        先描述,再组织!操作系统先将文件用结构体描述起来,再通过某个数据结构将这些文件结构体管理起来,这与我们前面所学的进程PCB控制块、进程地址空间、页表等内核数据一样,连管理思想都相同!在Linux中,这个结构体叫做 file,源码在Linux include/linux/fs.h 中定义,如下图;

Linux | 文件系统_第10张图片

问题:fd 是什么?

        按前面的说法,fd是文件描述符,是管理一个文件的句柄,这种理解是不全面的;下面我带着大家从源码的角度,一步一步剖析fd的本质;

        首先,当我们访问一个进程是必定会生成PCB、进程地址空间等内核数据结构;实际上还会为我们生成一个结构体,记录当前进程打开了的哪些文件等信息,这个结构体叫 files_struct,我们可以在源码的 include\linux\fdtable.h 文件中找到,如下图所示;

Linux | 文件系统_第11张图片

        仔细看,我们发现有一个结构体指针数组,叫做fd_array,实际上这个数组就是记录当前打开的文件,里面存放的就是文件的地址;而我们这个管理打开文件的结构体被记录到了PCB控制块,也就是说,我们可以通过PCB控制块找到这个结构体,我们直接去task_struct 中寻找即可验证,如下图;

Linux | 文件系统_第12张图片

        讲了这么多,那么fd到底是什么呢?实际上,fd就是我们files_struct 结构中,我们维护的那个当前进程已经打开文件数组fd_array的下标,捋一捋我们的思路,就是如下这张图;

Linux | 文件系统_第13张图片

问题:fd又与我们的C语言FILE结构体又有什么关联呢?

        我们可以肯定的是fd是操作系统层面的概念,而FILE结构体是C语言层面的概念;而C语言大部分是封装了系统调用,我们不难推测出FILE结构体中肯定有fd;

Linux | 文件系统_第14张图片

        C语言的源码中,FILE是被_IO_FILE重定义了,在_IO_FILE中,有一个成员_fileno,这个就是我们的文件描述符了;

2、文件描述符的分配规则

        前面我们已经对fd是什么有了深刻了解,实际上就是结构体指针数组的下标;我们通过这个下标可以找到这个指针数组里的指针,然后通过这个指针可以找到对应的文件;接下来,我们来了解文件描述符的分配规则,在了解这个知识点之前,我们依旧需要搞懂一下问题;

        Linux下有一个设计哲学,即Linux下一切皆文件;而这就有几个问题了;

问题:显示器是文件么?键盘呢?如果是文件,Linux是如何把这个硬件看做文件呢?

        既然说了,Linux下一切皆文件,那么毫无疑问,显示器和键盘也是文件,那么是如何使用 struct file 将其描述起来的呢?其实,不光我们的C++/Java这种语言有面向对象的属性,其实C语言可以,我们可以用C语言的结构体构建对象;如C++类中的成员变量,我们可以在结构体中设计变量来达到,而C++类中的成员方法呢?我们其实也可以使用函数指针来实现!这样C语言的结构体中既有了成员变量,又有了成员方法,这不就是对象面向中的类吗?既然可以用结构体表示,那我们的显示器和键盘等硬件也有方式用一个结构体来表示了,显示器文件有显示器文件的写方法,键盘有键盘文件的读方法,这样我们就可以用同一个结构体描述不同类型的文件,同样,硬盘、网卡等设备也可以通过此方法进行描述起来;

问题:既然显示器,键盘也是文件,也就是说我们平常打印就是往显示器文件里写,从键盘文件里读,可我们平时编写C语言代码从来没有打开过这两个文件啊,这又怎么解释呢?

        不知道,大家有没有听过stdin、stdout 和 stderr,这是C语言中的三个流,分别叫做标准输入流、标准输出流和标准错误流,分别对应 键盘,显示器,显示器;其实它们就是FILE*类型的,默认情况,这三个流是打开的,比如我们平常向显示器打印数据,使用下面这两行代码是没有任何差异的,都是向显示器打印字符串;

const char* str = "hello world\n";
printf("%s", str);
fprintf(stdout, "%s", str);

        既然这三个流是默认打开的,也就是说,这键盘和显示器这两个设备所对应的文件是默认已经加载进内存中了,既然已经在内存中,那么一定已经有了对应的 struct file 结构体,那么我们这个进程中 fd_array 数组中默认就有着三个文件指针,也就是说它们的结构如下图所示;

Linux | 文件系统_第15张图片

        也就是说,我们使用open打开一个文件,fd都是从3开始分配的!!那么我们可以写代码验证一下,是否如我们所料;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd1 = open("log.txt", O_RDONLY, 0666);
    int fd2 = open("log.txt", O_RDONLY, 0666);
    int fd3 = open("log.txt", O_RDONLY, 0666);
    int fd4 = open("log.txt", O_RDONLY, 0666);
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}

        运行结果如下;

Linux | 文件系统_第16张图片

        果然,如我们所料,进程描述符从3开始分配;那我们再进行如下测试;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 先关掉0号和2号文件描述符
    close(0);
    close(2);
    // 再观察文件描述符是如何分配的
    int fd1 = open("log.txt", O_RDONLY, 0666);
    int fd2 = open("log.txt", O_RDONLY, 0666);
    int fd3 = open("log.txt", O_RDONLY, 0666);
    int fd4 = open("log.txt", O_RDONLY, 0666);
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}

        测试结果如下;

Linux | 文件系统_第17张图片

        此时我们便可以得出最终结论,文件描述符是从低到高寻找没有用到的fd,一旦找到直接分配;

3、理解重定向

        有了上面的基础理论,我们可以写出如下代码;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 先关掉1号文件描述符(标准输出)
    close(1);
    // 再打开log.txt文件(这个文件的文件描述符肯定是1)
    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    // 使用printf打印数据
    printf("hello, you can see me!!!\n");
    // 使用fprintf函数向标准输出打印数据
    fprintf(stdout, "hello, I am fprintf\n");
    // 使用write函数向1号文件描述符打印
    const char* str = "hello, I am write\n";
    write(1, str, strlen(str));
    
    
    //close(fd1);  // 这里需要注释,具体原因,后面解释
    return 0;
}

        我们编译运行该程序,结果如下;

Linux | 文件系统_第18张图片

        我们发现,我们原本向显示器打印的数据并没有输出到显示器上,而是输出到了log.txt文件里!这不就是我们的输出重定向吗?这是否意味着虽然我们底层改变了文件描述符1下标里的指针,但是我们上层并不知道也并不关心,上层只直到当调用printf函数时往1号文件描述符对应的指针里写入,至于1号文件描述符里对应指针是否为显示器文件,它们并不关心!那我们再写出如下代码;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 先关掉0号文件描述符(标准输出)
    close(0);
    // 再打开log.txt文件(这个文件的文件描述符肯定是0)
    int fd1 = open("log.txt", O_RDONLY, 0666);
    // 接收缓冲区
    char* buf[1024];
    // 接收数据
    ssize_t sz = read(0, buf, sizeof(buf) - 1);
    buf[sz] = '\0';
    printf("%s", buf);
    
    
    close(fd1);
    return 0;
}

        这里从0号文件描述符,本来打算从标准输入中读取,可这不就从标准log.txt文件中读取数据了吗?这不就是我们的输入重定向吗?同样,追加重定向只需要更改一下打开文件的方式,把清空O_TRUNC更改为追加O_APPEND即可;

        其实上面的功能,我们可以通过一个系统调用来实现,这个系统调用如下所示;

Linux | 文件系统_第19张图片

        就是我们的dup2函数;这个dup2函数的使用十分古怪,他的作用是使newfd所对应的指针成为oldfd所对应指针的拷贝,也就是说,将fd_array数组中下标newfd的值,改为old下标对应的值;因此我们的输出重定向也可以这么写;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 再打开log.txt文件(这个文件的文件描述符肯定是0)
    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    // 输出重定向工作
    dup2(fd1, 1);
    // 接收缓冲区
    char* buf[1024];
    // 写入数据
    printf("hello world\n");
    
    
    close(fd1); // 这里无需注释也可以实现重定向
    return 0;
}

四、对缓冲区的理解

1、缓冲区引入

        首先,我们用一段代码来引出我们缓冲区的概念;如下所示;

#include 
#include 

int main()
{
    printf("hello world 1\n");
    printf("hello world 2");
    sleep(5);
    return 0;
}

        我们编译运行上述代码;

Linux | 文件系统_第20张图片

        我们发现刚开始前5秒钟,如图一,只有一个打印信息,也就是没有换行的那一句printf函数,而第二个打印函数看起来好像是运行结束前再打印出来的,整个代码执行顺序就好像是先执行第一个printf函数,再执行sleep函数,最后执行第二个printf函数,可我们的程序不应是顺序执行吗?难道我们之前学的都是错误的?这就与我们接下来学的缓冲区相关了;

2、缓冲区相关概念

问题:缓冲区是什么?

        所谓缓冲区,就是一段内存空间,暂时存放数据的一块地方;

问题:为什么要有缓冲区呢?

        理解这个之前,我们首先要理解计算机写入数据的两种模式;

写透模式:所谓写透模式,就是直接将数据写到磁盘后再返回;其成本,速度也慢;

写回模式:所谓写回模式,就是将数据写到缓存种就返回,剩下的由缓存再写入磁盘;写回模式效率高,但是CPU硬件实现也更复杂;

        而为了提高整机效率,我们采用写回模式,这里的缓存,就可以是我们的缓冲区;因为缓冲区的存在主要是为了提高效率,另一方面提高用户的响应速度;

3、C语言文件函数 VS 系统调用

        我们前面学习过printf、fprintf、fputs等函数,那么这些函数与write函数有什么关系呢?实际上,当我们调用C语言写入函数时,只是将数据写入到了C语言的缓冲区中,而当我们调用write函数时,我们才会将缓冲区数据写入内核中,最后写入磁盘;

4、缓冲区的刷新机制

        缓冲区一共有三种刷新机制,分别为立即刷新、行缓冲刷新、全缓冲刷新;

立即刷新:一旦写入缓冲区就立刻调用write函数写入内核;

行缓冲刷新:一旦遇到换行符就刷新;

全缓冲刷新:只有缓冲区满才刷新

        当然,我们也不是完全按照上述刷新缓冲区,总会有一些特殊情况;如

进程退出:退出前会刷新缓冲区

强制刷新:我们可以调用fflush强行刷新缓冲区

 Linux | 文件系统_第21张图片

补充:

        一般来说,对于显示器文件来说,选择行缓冲,为什么不选择立刻刷新和全缓冲呢?首先,对于立刻刷新来说,消耗太多系统资源,过多的IO会使效率降低,我们每次刷新到磁盘,实际上就是一次IO,而IO每IO一次都会等待很长时间(速度对于CPU来说),所以不选择立刻刷新;显示器对于用户来说使直接接触的,对于用户来说,若刷新太慢会影响用户体验,所以我们不选择全缓冲;

        对于普通文件来说,我们都偏向于全缓冲,首先,普通文件不像显示器文件,如磁盘,我们慢一点刷新,也不会特别影响用户体验,其次,全缓冲可以减少IO次数,大大提高整体效率;

回到缓冲区引入:

        当时我们写了一段代码,如下图所示;

Linux | 文件系统_第22张图片

        当时,我们看到的现象是先执行28行,再休眠5秒,最后执行29行代码;有了上面缓冲区刷新策略,我们就明白了,并不是执行顺序发生了改变,而是跟缓冲区刷新策略有关;printf函数是像显示器文件进行输出打印,而我们28行代码中,有换行,故会刷新到内核,接着刷新到显示器中,而我们调用29行函数时,并没有刷新缓冲区,此时数据仍然再缓冲区中,接着我们休眠5秒,接着我们要退出程序了,故我们要先刷新缓冲区数据,接着打印出29行应该打印的数据,给我们一种好像29行代码在后面执行的感觉,我们稍微修改上述代码,在29行后加上强制刷新函数,如下图所示,就会按我们想要的样子执行了;

Linux | 文件系统_第23张图片

Linux | 文件系统_第24张图片

Linux | 文件系统_第25张图片

5、再次深入理解缓冲区

        我们再来看一段代码;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    const char* str1 = "hello printf";
    printf("%s\n", str1);
    const char* str2 = "hello fprintf";
    fprintf(stdout, "%s\n", str2);
    const char* str3 = "hello fputs\n";
    fputs(str3, stdout);
    const char* str4 = "hello write\n";
    write(1, str4, strlen(str4));

    // fork();
    return 0;
}

        首先,我们将上述代码种,最后一个fork注释掉;接着编译运行,并运行重定向输出到log.txt文件中,结果如下所示;

Linux | 文件系统_第26张图片

        我们将fork注释取消,再如上编译运行,结果如下;

Linux | 文件系统_第27张图片

        神奇的一幕出现了!我们发现除了系统调用write,其他的打印函数居然打印了两次,可是我们的fork不是在最结尾吗?是在所有打印函数的后面!为什么加上fork后会有如此神奇的现象呢?这还是跟我们的缓冲区有关!

现象解释:

        当我们加上fork函数时,打印到显示器上,由于显示器文件是行缓冲,因此由于上述C语言写入函数与系统调用都带换行,因此最终结果每个函数都打印执行一次,可是当我们加入重定向后,写入到普通文件,此时变成了全缓冲,因此不会刷新到文件中,而是在C语言文件缓冲区中,而我们的write函数是系统调用,根本不存在C语言缓冲区概念,因此直接刷新到了文件中,这也就解释了,明明我们的write函数是最后调用的,却是第一个刷新到文件中来的;这是我们又执行fork函数,此时有了父子两个进程,当它们任意一个进程快要结束前,都会将缓冲区数据刷新到文件中,此时便会发生写时拷贝,故C语言相关写入函数的写入数据会刷新两次,故就有了上面的现象了;

回到理解冲顶向:

        当时,我们留下了一个伏笔,就是下面我们代码中,我们实现重定向时,不能调用close文件,否则重定向输出后的文件内容只有write函数打印的内容;

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 先关掉1号文件描述符(标准输出)
    close(1);
    // 再打开log.txt文件(这个文件的文件描述符肯定是1)
    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    // 使用printf打印数据
    printf("hello, you can see me!!!\n");
    // 使用fprintf函数向标准输出打印数据
    fprintf(stdout, "hello, I am fprintf\n");
    // 使用write函数向1号文件描述符打印
    const char* str = "hello, I am write\n";
    write(1, str, strlen(str));
    
    
    //close(fd1);  // 这里需要注释,具体原因,后面解释
    return 0;
}

 Linux | 文件系统_第28张图片

        若我们在最后面调用close后,由于我们现在1号文件描述符已经是普通文件了,因此刷新策略为全缓冲,在我们调用close函数时,此时C语言文件缓冲区中,还有内容没有刷新,而我们调用close函数关闭对应的文件时,在退出前,我们的C语言缓冲区里的内容向刷新到指定文件,可是1号文件描述符对应的文件已经被关闭了,因此无法正常刷新;

6、模拟实现C语言缓冲区

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define NUM 1024

typedef struct MyFILE
{
    int _fd;
    char _buf[NUM]; 
    size_t _end;
}MyFILE;


MyFILE* myopen(const char* filename, const char* mode)
{
    assert(filename);
    assert(mode);
    // 申请文件描述符
    MyFILE* pf = (MyFILE*)malloc(sizeof(MyFILE));
    if(pf == NULL)
    {
        perror("myopen");
        return NULL;
    }
    memset(pf, 0, sizeof(MyFILE));
    // 根据选项初始化
    if(strcmp(mode, "r") == 0)
    {
        pf->_fd = open(filename, O_RDONLY);
    }
    else if(strcmp(mode, "r+") == 0)
    {
        pf->_fd = open(filename, O_RDONLY);
    }
    else if(strcmp(mode, "w") == 0)
    {
        pf->_fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); 
    }
    else if(strcmp(mode, "w+") == 0)
    {
        pf->_fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0666); 
    }
    else if(strcmp(mode, "a") == 0)
    {
        pf->_fd = open(filename, O_WRONLY | O_CREAT | O_APPEND); 
    }
    else if(strcmp(mode, "a+") == 0)
    {
        pf->_fd = open(filename, O_RDWR | O_CREAT | O_APPEND);
    }
    else 
    {
        free(pf);
        printf("错误参数\n");
        return NULL;
    }
    return pf;
}

// 前置声明
void myfflush(MyFILE* stream);

int myfputs(const char* s, MyFILE* stream)
{
    assert(s);
    assert(stream);
    strcpy(stream->_buf + stream->_end, s);
    stream->_end += strlen(s);
    // 显示器文件,行缓冲
    if(stream->_fd == 1)
    {
        if(stream->_buf[stream->_end - 1] == '\n')
        {
            myfflush(stream);
        }
    }
    return strlen(s);
}


void myclose(MyFILE* stream)
{
    assert(stream); 
    // 关闭文件前刷新磁盘数据
    myfflush(stream);
    // 关闭文件描述符
    close(stream->_fd);
    // 释放申请空间
    free(stream);
}

void myfflush(MyFILE* stream)
{
    assert(stream);
    if(stream->_end != 0)
    {
        // 刷新到内核
        write(stream->_fd, stream->_buf, stream->_end);
        // 刷新到文件
        syncfs(stream->_fd);
        stream->_end = 0;
    }
}

int main()
{
    MyFILE* pf = myopen("log.txt", "w");
    if(pf == NULL)
        exit(1);
    myfputs("hello 1", pf);
    myfputs("hello 2\n", pf);
    myfputs("hello 3", pf);
    myfputs("hello 4\n", pf);

    fork();
    
    myclose(pf);
    return 0;
}

        我们也可以使用代码来测试是否有打印两次的现象;如上代码;

你可能感兴趣的:(Linux,linux)