当前位置: 首页 > article >正文

Linux——文件与内存

    我们很早就已经叙述过一个客观事实,即一个文件由文件内容和文件属性两部分组成。在过往的实践中,我们在C语言中有着一套对于文件的访问的方法,可以注意到我们使用C语言访问文件时总是会调用fopen函数来打开一个文件,而这一步打开文件正是访问所不可或缺的一步。

        所谓打开文件,其实质是将文件加载到内存。因为正常情况下文件存储在磁盘上,即处在外设中,当我们将要访问文件时,实际上是进程在访问文件。由于cpu不可以直接与外设进行数据交互,所以想要对文件进行操作,不可避免的要将文件加载到内存之中。于是就可以明白文件操作会牵涉到进程与被打开的文件二者之间的关系的。

        一个进程可以打开多个文件,而同时有多个进程时,所涉及的文件数目会更庞大,因此操作系统需要对文件进行管理,这也就是OS的文件系统。根据文件是否被打开(是否载入内存)可以分为被打开的文件和未被打开的文件。接下来重点叙述被打开的文件的相关特性与管理方法。

1. C语言中的文件操作

1.1 文件打开与关闭

        有关C语言中的文件打开、关闭、读写等操作可以参考我之前的文章,我分了三篇文章较为详细地解释了“文件的打开和关闭”(链接1),“顺序读写”(链接2),“随机读写”(链接3)。以下简单给出fopen函数的参数含义。

        

1.2 读写函数

        接下来借此机会汇总一下常用的C标准库的IO函数。

        一般而言从函数名就可以看出其功能了,f-(file)前缀表示对文件流的操作,s-(string)前缀表示对字符串的操作,n-(number)前缀表示需要指定字符个数,-f(format)后缀表示格式化参数,-c(character)后缀表示单个字符,-s(string)后缀表示单个字符。

1.2.1 printf

int printf(const char *format, ...); 格式化写入stdout标准输出流

int fprintf(FILE *stream, const char *format, ...); 格式化写入指定stream流

int sprintf(char *str, const char *format, ...); 格式化写入指定str字符串

int snprintf(char *str, size_t size, const char *format, ...); 格式化写入指定str字符串n个字符

1.2.2 scanf

int scanf(const char *format, ...); 格式化读取stdin标准输入流

int fscanf(FILE *stream, const char *format, ...); 格式化读取指定stream流

int sscanf(const char *str, const char *format, ...); 格式化读取指定str字符串

1.2.3 put

int fputc(int c, FILE *stream); 单个字符写入指定stream流

int fputs(const char *s, FILE *stream); 字符串写入指定stream流

int putc(int c, FILE *stream); 单个字符写入指定stream流

int putchar(int c); 单个字符写入stdout标准输出流

int puts(const char *s); 字符串写入stdout标准输出流

1.2.4 get

int fgetc(FILE *stream); 从指定stream流中读取单个字符

char *fgets(char *s, int size, FILE *stream);  从指定stream流中读取字符串(指定长度个字符)

int getc(FILE *stream); 从指定stream流中读取单个字符

int getchar(void); 从stdin标准输入流读取一个字节

char *gets(char *s); 从stdin标准输入流读取字符串

int ungetc(int c, FILE *stream); 从指定stream流中读取一个字符,并推回(即读取字符后,将字符再推回缓冲区中,相当于读但不读取,以达成读取字符但文件流指针不移动的假象

2. 系统调用接口

2.1 open

参数含义

pathname:表示打开文件的路径,可以是相对路径或绝对路径。

flags:打开文件的方式。

        注:falgs作为一个int类型的参数,对其传参的方式是采取宏之间按位或的方式。这是将int类型的flags看作一个位图,每一位代表着不同的特性,提前将各种不同的功能参数封装成宏之后,相互取或即可完成组合。以下是常用的几种宏:

        O_RDONLY:以只读方式打开文件。
        O_WRONLY:以只写方式打开文件。
        O_RDWR:以读写方式打开文件。
        O_CREAT:如果文件不存在,则创建该文件。
        O_EXCL:与O_CREAT一起使用,确保创建文件时文件不存在。
        O_TRUNC:如果文件已存在,打开时将其截断为0长度。
        O_APPEND:以追加模式打开文件,写入操作将始终在文件末尾进行。
        O_NONBLOCK:以非阻塞模式打开文件,这对于设备文件尤其重要。
        O_DSYNC:以同步方式打开文件,保证数据写入后立即刷新到存储介质。
        O_SYMLINK:当目标是一个符号链接时,直接打开符号链接,而不是它所指向的文件。

mode:创建出的文件的权限。对于一个目录文件是0777,而对于一个普通文件则是0666.

        注:需要指出,我们发现有两个open接口,它们的区别就是有无mode参数。

        open(const char *pathname, int flags)——此版本的 open 接口用于打开一个已存在的文件。flags 参数指定了打开文件的方式(如只读、只写等)。如果文件不存在,则会返回错误。

        open(const char *pathname, int flags, mode_t mode)——此版本的 open 接口用于在创建新文件时使用。它包含一个额外的 mode 参数,这个参数用于设置新创建文件的权限位。

        另外我们之前介绍权限的时候也说过,文件实际的权限是和umask有关的,所以最后创建出的文件权限应该考虑到掩码的存在,即为(mode&(~umask))。umask默认为002,可以通过mode_t umask(mode_t mask);函数进行修改。

2.2 close

        close的作用是关闭一个文件描述符fd,关于文件描述符的问题我们会在后文具体说明。

2.3 write

        write的作用是在一个文件描述符上执行写操作。

fd:待写入的文件描述符

buf:待写入内容所在的缓冲区,buf是一个void类型的指针,指向缓冲区开始位置

count:待写入内容的长度

2.4 read

        read的作用是在一个文件描述符上执行读操作。

fd:待读取的文件描述符

buf:将读取的内容放入的缓冲区,buf是一个void类型的指针,指向缓冲区开始位置

count:待读取内容的长度

        以下给出上述接口的使用示例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstring>
int main()
{
     //以只写的方式打开log.txt,如果不存在则创建,创建的初识权限为0666,如果存在则截断原文件为空
    int fd1 = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    char buf[64] = "hello world";
    write(fd1,buf,strlen(buf));//将buf中的内容写入fd1
    //注意:字符串以'\0'结尾只是c语言的要求,写入文件不需要写入'\0'
    close(fd1);

    //以只读的方式打开log.txt
    int fd2 = open("log.txt",O_RDONLY);
    char ret[64];
    ssize_t length = read(fd2,ret,sizeof(ret)-1);//将fd2内容读到ret中
    if(length>0)//read返回值是读取到的字符个数,大于0说明读取成功
    {
        ret[length]='\0';
        printf("%s\n",ret);
    }
    close(fd2);
    return 0;
}

output:

3. 文件描述符

        我们一直在说的文件描述符,实际上并没有什么神秘的,通过上述接口的返回值和参数,我们可以发现,fd实际上就是一个整数。文件描述符是一个小整数,用来指向和标记打开的文件,因此我们在open打开文件后会返回文件描述符,而对文件的操作可以通过参数fd来指定文件。

3.1 标准文件描述符

        对于任意一个进程,在程序启动时会默认打开三个文件描述符,而这三个文件描述符则被称为标准文件描述符

标准输入stdin),默认来源是键盘,文件描述符是0,通常用于读取用户的输入数据。

标准输出stdout),默认来源是显示器,文件描述符是1,通常用于向用户显示输出信息。

标准错误stderr),默认来源是显示器,文件描述符是2,也通常用于输出错误信息,以便与标准输出区分开来。

3.2 Linux对进程与文件的管理

        假设进程打开了一个名为log.txt的文件,那么内存中对应的布局如下图所示。

        ①进程和文件需要被关联,一个进程可以打开多个文件,但是进程管理和文件管理各司其职,因此需要解耦并通过诸如指针等方式建立联系。

        ②对于进程管理我们已经不陌生了。Linux内存中存在着task_struct作为进程管理中进程的描述,而task_struct通过链表的结构被组织了起来。

        ③在进程的PCB中,有一个叫做files的files_struct的结构体指针,这个指针指向了一个files_struct的结构体,其中存储着该进程所打开的文件的相关信息

        ④files_struct结构体便是进程和文件联系的关键,其中包含了一个结构体指针数组,数组中的每个元素都是files结构体的指针

        ⑤files结构体则是存在于内存中对于文件的描述,files结构体之于文件,相当于task_struct之于进程。

        有了这样的结构认识后,当log.txt被进程A打开后,进程A的PCB中的struct files_struct* files成员所指向的struct files_struct结构体内的结构体指针数组就会新增一个元素,其值是一个指向描述log.txt的struct file结构体。

3.2.1 文件描述符的实质

        在对文件和进程在内存中的布局有一个大致认识之后,就要揭开文件描述符的真实面纱了。所谓文件描述符,实质上就是files_struct结构体内的结构体指针数组的下标

        我们介绍的的系统调用接口访问文件都是通过fd进行的。例如访问上图中的log.txt文件,拿到fd是3。然后就可以通过进程自己的task_struct结构体找到管理进程打开的文件的结构体,通过指定的fd即可在该结构体中找到数组指定下标的元素,通过这个拿到的值即可找到文件结构体。

        需要补充指出的是,struct files_struct* files字段是task_struct的一部分,即每一个进程都有自己独有的files_struct,其中记录着该进程打开的文件,所以每一个进程的fd对应的内容都是不同的。

3.2.2 文件描述符的分配

        文件描述符的值是数组下标,所以当打开文件时,会分配一个当前唯一的值作为该文件的fd。直接给出结论,文件描述符的分配方式是从小到大,找到第一个空闲位置,其下标作为文件描述符

        我们之前提到,程序启动时会默认打开三个标准文件描述符,因此对于任何进程刚刚启动后,数组中的0、1、2位置一定被分配给了这三个文件。因此才在C语言中直接定义stdin=0,stdout=1,stderr=2,默认将三者与0、1、2捆绑。也是因为前三个位置被占据,所以我们打开的文件其fd就从3开始了。

        如此再看文件描述符也没什么神秘的,也就是一个非负整数,表示文件结构体指针在数组中的下标。当close关闭一个文件时,也会将数组中的指定位置空间清空,再结合文件描述符从小到大的分配顺序,于是就会出现在close(1)之后,1的位置空闲,此时打开的文件log.txt的fd就是1了。此时再使用stdout,就会发现输出到了log.txt中而非屏幕了。这也体现了C语言中stdin,stdout,stderr实际上是硬编码赋值0、1、2(毕竟默认打开,很少被用户主动关闭)。

3.2.3 Linux一切皆文件

        linux一切皆文件这句话我们不止一次在说了,上面提到的键盘stdin和显示屏stdout都被作为和普通的文件一样来操作,这就是虚拟文件系统(VFS)的功能。VFS是操作系统中的一种抽象层,用于管理不同类型的文件系统。它提供一个标准化的接口,使得不同类型的文件系统可以通过相同的 API 进行访问。这意味着无论是针对何种文件,用户都可以使用相同的方法来读写文件。

        驱动是一种特殊类型的软件,负责与计算机硬件设备进行通信。它充当操作系统和硬件之间的桥梁,使得操作系统能够控制和管理各种硬件设备。

        对于硬件的操作与控制需要通过驱动程序来进行,而驱动程序则是封装了底层的硬件,向上提供具体可操作的接口。在打开文件的file结构体中有着对文件读写操作的函数指针,根据这个函数指针即可找到各自的读写方法。

        可以说驱动程序将复杂的硬件操作抽象为标准化的接口,提供给操作系统或用户应用程序使用。这样,应用程序无需关心底层硬件的具体实现,只需要通过驱动提供的接口调用硬件功能。紧接着VFS虚拟文件系统封装了底层具体文件系统的实现细节,为用户和应用程序提供了统一的文件访问接口。这样的封装对上层去除了差异性和底层细节,于是我们才可以有一切皆文件的大一统视野。

        各种封装的构成也大大加强了语言的可移植性。尽管可能底层的硬件不同,驱动实现方式有差异,但是在封装后,只要保证向上接口的操作方式一致,即可正确执行。C语言库函数中的fopen等等函数也就是封装了open等系统调用,当更换设备甚至操作系统,尽管底层实现大相径庭,但是不同系统都有对应的C标准库,其中对底层的封装大不相同,但是却提供了一致的接口。因此C语言在A系统下调用A的库可以编译并运行,到了B系统下,编译用的就是B系统的库,因而也可以执行。

3.3 重定向

        我们对重定向并不陌生,曾在指令汇总篇章介绍过在shell中重定向的使用方法,所谓重定向就是改变输入或输出的方向。Linux的shell中重定向包括 >:输出重定向>>:追加重定向<:输入重定向

        在刚刚了解了文件描述符的基础上,我们可以知道内容写入到哪里实际上是通过文件描述符控制的。而文件描述符可以被更改,所以写入内容的方向也就自然而然可以被我们更改了。以下述代码为例。

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstring>
int main()
{
    close(1);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);

    printf("fd1:%d\n",fd1);
    printf("fd2:%d\n",fd2);

    fflush(stdout);

    close(fd1);
    close(fd2);

    return 0;
}

        首先关闭了标准输出,此时1这个文件描述符就空闲出来了,于是之后打开的log1.txt就会占据这个位置。因为printf打印实际上是对stdout进行写入,而stdout是1已经被新的文件占据了,于是后文的printf写入就全部写入到了log1.txt之中。

3.3.1 dup2系统调用

        dup2的作用就是将将一个文件描述符(oldfd)覆盖到另一个指定的文件描述符(newfd)上。这个函数会关闭被覆盖的文件描述符。

oldfd:是覆盖者,是要留下来的fd

newfd:是被覆盖者,是要被关闭的fd

        通过dup2函数我们可以直接通过dup2(fd, 1)实现关闭原来的1文件描述符,并且把1这个位置替换为fd对应的文件。

 3.3.2 shell实现重定向

        重定向的本质就是更改标准输入输出的文件描述符,将其覆盖为我们所希望的文件。因此我们可以在之前完成的shell的基础上增加重定向功能,通过bup2系统调用,输入重定向用指定fd覆盖stdout,而输入、追加重定向则是用指定fd覆盖stdin

        补充的代码如下,定义了重定向的类型与文件,并且由于重定向符号前后的空格数可以有若干个,所以使用了宏来实现去除多余括号的功能,以保证拿到正确的文件名。

//定义重定向类型
enum{
    NONE_REDIRECT = 0,
    INPUT_REDIRECT = 1,
    OUTPUT_REDIRECT = 2,
    APPEND_REDIRECT = 3
};

int redirect = NONE_REDIRECT;//重定向类型
char* filename = nullptr;//重定向文件

#define TrimSpace(pos) do{\
    while(isspace(*pos)) pos++;\
    }while(0) //使用do-while框住代码段,保证隔离

        含有重定位的指令一定是由两部分组成并且由重定位符号分隔。前一部分是需要执行的指令,而后一步则是具体重定位到的文件。所以在解析指令的过程中,需要将这两部分分开。接下来进入执行阶段,dup2重定位到指定文件中,指令部分继续被执行。

        需要注意的是重定位涉及到对标准输入输出的文件描述符覆盖,所以为了不对shell自身产生影响,应将含有重定位的指令放到子进程中进行执行。作为子进程,它具有父进程的代码数据的拷贝,所以其内核中的文件描述符表、地址空间、页表等都别无二致,所以重定向可以正常进行。

void ParseCommandLine(char* buffer)
{
    //新的指令,初始化上一次的全局变量
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    redirect = NONE_REDIRECT;
    filename = nullptr;

    //边历寻找重定位符号:<、>
    //ls -a -l > file1
    //1.重定位符号前的内容作为指令执行
    //2.重定位符号后的内容指示重定位文件
    int cur = 0;
    while(buffer[cur])
    {
        if(buffer[cur] == '<')
        {
            redirect = INPUT_REDIRECT;
            buffer[cur] = 0;//截断指令部分,以执行指令
            filename = &buffer[cur + 1];//文件名
            TrimSpace(filename);//去除文件名的空格
        }
        else if(buffer[cur] == '>')
        {
            if(buffer[++cur] == '>')
            {
                redirect = APPEND_REDIRECT;
                buffer[cur] = buffer[cur-1] = 0;
                filename = &buffer[cur+1];
                TrimSpace(filename);
            }
            else 
            {
                redirect = OUTPUT_REDIRECT;
                buffer[cur-1] = 0;
                filename = &buffer[cur];
                TrimSpace(filename);
            }
        }
        else 
        {
            cur++;
        }
    }

    const char* sep = " ";
    gargv[gargc++] = strtok(buffer, sep);
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;
}
//外部命令由子进程执行
bool ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) return false;
    if(id == 0)
    {
        //子进程
        //重定向的工作会涉及到覆盖文件描述符背后的文件,所以应该交给子进程完成
        if(redirect == INPUT_REDIRECT)
        {
            if(filename)
            {
                int fd = open(filename,O_RDONLY);
                if(fd<0)
                {
                    exit(10);
                }
                dup2(fd,0);
            }
            else 
            {
                exit(11);
            }
        }
        else if(redirect == OUTPUT_REDIRECT)
        {
            if(filename)
            {
                int fd = open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
                if(fd<0)
                {
                    exit(10);
                }
                dup2(fd,1);
            }
            else 
            {
                exit(11);
            }
        }
        else if(redirect == APPEND_REDIRECT)
        {
            if(filename)
            {
                int fd = open(filename,O_CREAT|O_WRONLY,0666);
                if(fd<0)
                {
                    exit(10);
                }
                dup2(fd,1);
            }
            else 
            {
                exit(11);
            }
        }
        execvpe(gargv[0], gargv, genv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        if(WIFEXITED(status)) //WIFEXITED:判断子进程是否正常退出
        //正常退出指通过exit或到达主程序结尾而结束,与之相对的是由信号进行终止
        {
            lastcode = WEXITSTATUS(status); //WEXITSTATUS:获得子进程的退出码
        }
        else 
        {
            lastcode = 178;
        }
        return true;
    }
    return false;
}

 4. 缓冲区

        对于如下这段代码,首先关闭了stdin,然后打开了log1.txt,换言之就是log1.txt这个文件的文件描述符为1,所以两条printf语句都会将内容打印到log1.txt中。如果没有fflush会发现文件中没有这两句内容,而加入了fflush之后才能正常写入文件中。这就是缓冲区存在的缘故。

        

4.1 用户空间缓冲区

        当调用库函数(如printf)时,数据首先被存储在用户空间的缓冲区内。这个缓冲区是由C标准库(FILE结构)管理的,其本质就是在内存中开辟了一块空间用于数据的缓存,当写入数据时先滞留在缓冲区内,然后再伺机将这块空间内全部内容写入内核中。

        想要将用户空间缓冲区的数据写入内核缓冲区,要考虑其刷新策略。常见的刷新策略有如下三种:

        全缓冲:只有在缓冲区满时或文件关闭时才会将数据写入底层系统。普通文件采取这种策略。
        行缓冲:通常在交互式终端上使用,遇到换行符或者调用fflush时刷新缓冲区。显示器采取这种刷新策略。
        无缓冲:每次写入都会立即发送到目标设备,不经过缓冲。
        总结一下,用户空间缓冲区刷新需要:①遇到换行符(对于行缓冲);②缓冲区满;③调用fflush()函数;④关闭流(通过fclose())

        用户空间缓冲区存在的作用和所有缓冲区的意义相同,都是为了保证数据的高效交换。数据写入内核本质上也只是拷贝,从用户空间写入内核就需要调用系统调用接口。如果遇到数据就直接写入内核,那么多次系统调用的开销也不可小觑,这降低应用程序的响应速度。加入缓冲区后,可以将多个零散的数据一次写入,减少了直接与内核的交互频率。

4.2 内核缓冲区

        当用户空间的缓冲区被刷新时,数据会通过系统调用(如write)传递到内核态。此时,内核会有自己的缓冲机制来管理I/O操作,即由操作系统自身觉得何时将数据写入磁盘中。
        作为缓冲区的内核缓冲区同样用于提高性能,允许应用程序异步处理I/O。这意味着应用程序可以继续执行而不必等待所有数据完全写入磁盘。

4.3 缓冲区小结

        我们在理解了上述两种缓冲区的情况下,再来看这段代码。

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

int main()
{
    printf("printf()\n");
    fprintf(stdout,"%s","fprintf()\n");
    fputs("fputs\n",stdout);
    write(1,"write\n",strlen("write\n"));

    fork();
    return 0;
}

         这段代码的功能就是打印字符串到stdout中,但是具体的执行结果会有不同。

        当直接执行可执行程序时,程序按预想的将字符串打印了出来。

        对于直接执行程序的情况,调用的四个函数都是向stdout标准输出写入字符串,所以没有什么意外的。

        当执行可执行程序加入重定向到文件中时,文件的内容却和预想的不太一样。

        我们会发现文件中的内容以及顺序有了一些变化。总结为两方面:

        ①输出的顺序发生了改变,先打印了write,再打印了stdio中的库函数;

        ②只有write打印了一遍,其余的三个函数打印了两遍。

        产生这种现象的原因就是我们刚才介绍的缓冲区的作用。

        对于第一种没有重定向的情况,是向显示器中写入,因此用户空间缓冲区采取的是行刷新,由于所有的打印的字符串都有换行符,因此printf、fprintf、fputs在把字符串读入用户缓冲区后直接刷新到了内核缓冲区。最后write直接写入内核缓冲区,所以最后打印出的结果就是符合调用顺序的字符串。

        而第二种情况则是向文件中写入,输出重定向后stdout被log.txt文件取代,用户空间缓冲区采取全缓冲策略。直至fork函数前,用户空间缓冲区中有着printf、fprintf、fputs三者的字符串,而内核空间中有着write的字符串。所以等到进程将要结束时,把用户空间缓冲区的内容刷新到了内核缓冲区中,所以出现了先打印了write,再打印了stdio中的库函数的顺序的现象。

        并且,由于代码最后调用了fork接口,创建了一个子进程,子进程会拷贝父进程的所有代码和数据,所以同时也包括其用户空间缓冲区的全部内容、位于内核中的文件描述符表、页表等等。所以在父子进程结束的时候都会向内核缓冲区中刷新自己的用户空间缓冲区,因此出现了打印两遍的情况。

4.4 stdio的简单封装

        对于这个简单的stdio,我们使用系统调用来封装fopen、fwrite、fclose、fflush函数,以此来加深对缓冲区的理解。

mstdio.h

#pragma once
#include<stdio.h>

#define SIZE 1024

enum{
    FLUSH_NONE,
    FLUSH_ALL,
    FLUSH_LINE
};

typedef struct IO_FILE
{
    int flush_type;
    int fd;
    char buffer[SIZE];
    size_t Capacity;
    size_t Size;
}mFILE;

mFILE* mfopen(const char* filename,const char* mode);
size_t mfwrite(const void* buffer, size_t size, size_t count, mFILE* stream);
void mfflush(mFILE* stream);
void mfclose(mFILE* stream);

        我们之前说过,用户空间缓冲区是由FILE结构体管理的,而FILE结构体中包含了文件的各种属性。于是我们可以自己模仿出一个FILE结构体,其中包含缓冲区刷新方式、文件描述符fd、缓冲区buffer、容量和大小。

mstdio.c

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

mFILE* mfopen(const char* filename,const char* mode)
{
    int fd = -1;
    if(strcmp(mode,"r")==0)
    {
        fd = open(filename, O_RDONLY);
    }
    else if(strcmp(mode,"w")==0)
    {
        fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC,0666);
    }
    else if(strcmp(mode,"a")==0)
    {
        fd = open(filename,O_CREAT|O_WRONLY,0666);
    }
    
    if(fd<0) return NULL;
    mFILE* mf = (mFILE*)malloc(sizeof(mFILE));
    if(mf==NULL)
    {
        close(fd);
        return NULL;
    }
    mf->flush_type = FLUSH_LINE;
    mf->fd = fd;
    mf->Capacity = SIZE;
    mf->Size = 0;

    return mf;
}

size_t mfwrite(const void* buffer, size_t size, size_t count, mFILE* stream)
{
    memcpy(stream->buffer+stream->Size,buffer,size*count);
    stream->Size += size*count;

    if(stream->flush_type==FLUSH_LINE && stream->Size>0 && stream->buffer[stream->Size-1]=='\n') mfflush(stream);
}

void mfflush(mFILE* stream)
{
    if(stream->Size>0)
    {
        write(stream->fd,stream->buffer,stream->Size);
        stream->Size = 0;
    }
}

void mfclose(mFILE* stream)
{
    if(stream->Size>0) mfflush(stream);
    close(stream->fd);
}

        fopen函数以指定方式打开一个文件,所以底层调用的就是open接口,根据使用的参数不同,存在只读、只写、追加等多种方式。fopen最后的返回值应该是已经打开的文件的文件指针,也就是我们的结构体指针FILE*。

        fwrite函数向文件内写入数据,实际上写入的位置是用户空间缓冲区,也就是文件的FILE结构体的buffer,因此实质上只是一次数据拷贝。在写入缓冲区后需要根据刷新条件判断是否要刷新到内核级缓冲区。

        fflush函数将用户空间缓冲区的数据刷新到内核缓冲区,所使用的接口就是write。

        fclose函数在关闭文件描述符之前会将用户空间缓冲区的内容刷新到内核中,因此需要调用一次fflush。close接口因为是相较于库函数更底层的系统调用接口,所以自然不会主动刷新用户空间缓冲区到内核。


http://www.kler.cn/a/515979.html

相关文章:

  • (长期更新)《零基础入门 ArcGIS(ArcMap) 》实验六----流域综合处理(超超超详细!!!)
  • 学到一些小知识关于Maven 与 logback 与 jpa 日志
  • 【mptcp】ubuntu18.04和MT7981搭建mptcp测试环境操作说明
  • 最小距离和与带权最小距离和
  • 实施工程师:面试基础宝典
  • Linux shell 批量验证端口连通性
  • 【c语言日寄】Vs调试——新手向
  • 大模型 / 智能体在智能运维领域的应用总结与发展趋势概述
  • win32汇编环境,按字节、双字等复制字符的操作
  • uniapp+Vue3(<script setup lang=“ts“>)模拟12306城市左右切换动画效果
  • ssm基于SSM的毕业论文开题评审管理系统
  • 【力扣:新动计划,编程入门 —— 题解 ②】
  • 为什么Foreach循环中为什么不能使用 remove/add操作?
  • 网络(三) 协议
  • DC-DC稳压电源——实战(基于Ti5450芯片)基础知识篇(1)
  • Linux权限管理:从用户切换到文件权限
  • 【MYSQL】mysql 常用命令
  • java基础学习——jdbc基础知识详细介绍
  • JS-Web API -day06
  • Vue2 项目二次封装Axios
  • MYSQL学习笔记(五):单行函数(字符串、数学、日期时间、条件判断、信息、加密、进制转换函数)讲解
  • 如何选择TVS二极管?
  • Vue - toRefs() 和 toRef() 的使用
  • 准备知识——波纹度和粗糙度区别与联系
  • 【力扣】1312. 让字符串成为回文串的最少插入次数
  • 法律与认知战争:新时代的战略博弈