0


【Linux】一文看懂基础IO并模拟实现

img

Halo,这里是Ppeua。平时主要更新C语言,C++,数据结构算法…感兴趣就关注我吧!你定不会失望。

本篇导航

在这里插入图片描述

0. C语言的文件接口

我们在C语言时已经学过了文件调用的相关接口.来复习一下相关接口:

  1. 通过fopen来打开一个文件

image-20231122190650694

其包含在stdio.h的头文件当中.常用的有两种模式: w,a(清空再写入文件末尾进行追加写)

使用方法

#include<stdio.h>intmain(){constchar* path="./log.txt";
    FILE* f=fopen(path,"a");fclose(f);return0;}

如果当前工作目录(CWD)下没有该文件,则会创建并打开.若已存在,则直接打开

  1. 通过fwrite来对一个文件对一个文件进行读写

image-20231122193437468

向文件FILE stream写,从ptr处获取内容写,每次写nmemb * size 大小的内存.*

#include<stdio.h>#include<string.h>intmain(){constchar* path="./log.txt";
    FILE* f=fopen(path,"a");constchar* info="hello,linux\n";fwrite(info,strlen(info),1,f);fclose(f);return0;}

注意!!这里写入时strlen并不需要加一,也就是不需要将’\0’算上,因为’\0’是C语言用来分割字符串的方式,但这里是文件进行读写.所以并不需要加上’\0’的地址.

这样我们就成功向log.txt中写入了"hello,linux";

image-20231122194340298

1. 系统的文件接口

复习完C语言的文件调用接口,我们来学习一下系统的文件调用接口.

因为C语言的文件调用接口需要访问硬件资源,在很早之前谈操作系统的时候说过,系统并不相信任何人,如果需要访问其底层的硬件,只能通过系统级的接口来访问.类似的接口有fprint,printf,sprint等等

所以C语言的文件调用接口一定是对系统的接口进行了二次封装.

1.1 open打开文件

和C语言的fwrite大同小异.一样需要传入创建文件的路径 pathname, 之后需要传入flags操作符.再传入创建文件的权限信息mode(使用八进制).

flags 有以下几种:

  • O_APPEND – 追加写
  • O_CREAT – 如果文件不存在就创建
  • RDONLY – 只读
  • O_WRONLY – 只写
  • O_TRUNC – 先清空后写入.

这每个flag都是定义的宏,可以使用|的方式,实现传入多个选项.(每个bit位不同可以代表不同的功能)

其返回值为一个整形:file description 文件描述符.

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){constchar* path="./log.txt";
    FILE* f=fopen(path,"w");constchar* pathos="./logos.txt";int fd=open(pathos,O_CREAT|O_WRONLY|O_TRUNC,0666);close(fd);return0;}

所以,C语言中的W权限是对O_CREAT|O_WRONLY|O_TRUNC,进行的一个封装.

image-20231122200515866

由于此时系统中的umask为0002,所以对于group仅有可读权限.

1.2 write写入文件

image-20231122195015122

这个使用方法与fwrite几乎一样.

就不介绍了

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){constchar* pathos="./logos.txt";int fd=open(pathos,O_CREAT|O_WRONLY|O_TRUNC,0666);write(fd,pathos,strlen(pathos));close(fd);return0;}

即使我们写入了多次,文件中也仅存在一条内容.

image-20231122200934064

也可以说明,C语言中的W权限是对O_CREAT|O_WRONLY|O_TRUNC,进行的一个封装.

2. 文件系统介绍

那么上面提到的file description是什么呢? 为什么C语言使用了FILE*的结构体指针作为返回值而底层仅需要返回一个数字?这个数字是什么?

操作系统想要管理被进程已经打开的文件,肯定需要经过先描述再组织这一过程.

我们今天仅把视角放在进程Task_Struct如何管理自己已经打开的文件.而不去关心这个操作系统如何去管理整个文件系统的.

4e7e442d7cca1fe7e9cb0d0e15c3355

进程PCB中有一个管理已经打开的文件系统的指针.管理了一系列已经打开的文件.所以我们得到的整数返回值fd,就是我们当前文件被放在进程已经打开file_struct数组中的下标,我们通过这个下标就可以访问我们刚刚打开的文件,

文件fd每次都会从未用过的下标中的最小值开始分配.通常我们拿到的一般是3.

因为012都被系统中的 0. 标准输入 stdin, 1. 标准输出 stdout, 2. 标准错误 stderr分配了. 所以这三个标准输入输出流是系统的标准.

此外,虽然标准输出与标准错误的结果都是打印在屏幕上(共享屏幕这个资源),但关闭其中一个并不会影响另外一个.

因为对屏幕这个资源采用了引用计数(系统中很多共享资源都采用这个方案),关闭其中一个引用对象,只会导致引用计数–

可以来验证下这个结论.

我们可以直接向fd为1的文件写入数据,看看是否会出现在屏幕上

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){constchar* pathos="./logos.txt";write(1,pathos,strlen(pathos));return0;}

image-20231122203402445

我们将其关闭,重新分配文件描述符看是否为1

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){close(1);constchar* pathos="./logos.txt";int fd=open(pathos,O_CREAT|O_WRONLY|O_TRUNC,0666);fprintf(stderr,"fd is %d\n",fd);close(fd);return0;}

image-20231122204013093

这里用到了fprintf这个函数

image-20231122204230962

以特定格式向文件中写入内容.

2.1 如何理解一切皆文件?

我们需要有一套统一的方法去操控各样的硬件.

所以在设计操作系统时封装了三层.

  1. 最底层为对各种硬件进行描述,写操作的方法.
  2. 但是每一种硬件肯定都是不一样的.所以有了第二层.统一将每一种操纵每一种文件的方法进行了封装.封装函数指针指向第一层的具体方法,此时他们的名字都一样了
  3. 第三层就是暴露给用户的STRUCT_FILE里面封装了指向第二层具体操作方法,和其他描述这个文件的信息.

这就是C++中多态的设计灵感

fe362b05215d724d3cb8f8d3b5216a8

3. 输入输出重定向

我们前面介绍过,stdout的fd是1.如果我先将1关闭,之后打开一个文件,再向1中写入会发生什么呢?

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){close(1);constchar* pathos="./logos.txt";int fd=open(pathos,O_CREAT|O_WRONLY|O_TRUNC,0666);write(1,pathos,strlen(pathos));close(fd);return0;}

原本应该打印在屏幕上的信息,被写到了文件当中

image-20231125111603057

因为stdout先被关闭了,文件描述符的分配是从最小值开始,所以分配时又将1分配给了新的文件,所以再次向1中写入的时候就变成了向文件当中写入

这就是输入输出重定向的本质概念:将输入输出描述符分配给对应的文件,就可以达成输入输出重定向的原理

image-20231125112218475

我们也有一个系统调用接口dup2.将oldfd复制到newfd.

这里可能有点难理解,oldfd可以理解为src(原文件描述符),newfd可以理解为tar(目标文件描述符).把在原文件描述符里指向原文件的指针复制覆盖到目标文件描述符当中

66782e9db8a6fe19f6249844a477065

我们可以使用下面这份代码来实现上面的效果

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){constchar* pathos="./logos.txt";int fd=open(pathos,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);close(fd);write(1,pathos,strlen(pathos));return0;}

在重定向完就可以关闭原来分配到的描述符,也就是close(fd)

image-20231125113529579

所以我们之前在命令行中输入的

command<filecommand>>filecommand>file

都是一种重定向.

第一种是将输入流重定向到文件当中,也就是从文件当中读取内容放到命令中执行

二三种都是将文件的输出结果写入到文件当中而不是输出到屏幕上.

4. 用户缓冲区与系统缓冲区

要了解缓冲区这个概念,我们先来回顾一个概念.

之前提到过,计算机绝大多数都满足冯诺依曼这个体系,也就是数据参与计算(使用CPU之前),都需要将自己先放入到内存.

因为CPU运行速度远高于其他介质,所以需要从其他"矮个子里挑高个",找一个较快访问速度的存储介质.

那么我们数据原来都是在磁盘里存储的,要想将他写入到内存当中去**.首先面临一个问题,我是一有数据就写,还是等数据到达了一定规模在写进去呢?答案显然是后者**,因为这样仅需拷贝一次,而不需要多次拷贝.

缓冲区充当的就是存储还未到达一定规模的数据,并格式化用户输入的数据.

那么在我们日常使用的语言与系统中 缓冲区在哪里呢?

我们先来看看下面这个现象

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<unistd.h>intmain(){constchar* buff="hello linux\n";printf("hello io\n");fprintf(stdout,"hello linux i am %d\n",getpid());write(1,buff,strlen(buff));return0;}

内容会正常的被打印出来.

image-20231126125405142

如果这个改成这样

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<unistd.h>intmain(){constchar* buff="hello linux";printf("hello io");fprintf(stdout,"hello linux i am %d",getpid());write(1,buff,strlen(buff));close(1);return0;}

在进程结束前手动关掉标准输出流stdout,去掉了’\n’,结果会怎样呢?

image-20231126130734877

竟然只输出了write里的内容.

如果给其加上’\n’呢?

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<unistd.h>intmain(){constchar* buff="hello linux\n";printf("hello io\n");fprintf(stdout,"hello linux i am %d\n",getpid());write(1,buff,strlen(buff));close(1);return0;}

image-20231126130901900

我们发现,C语言的接口,不加’\n’是不会被刷新的.

C语言有自己的缓冲区(用户缓冲区),在该缓冲区刷新之前不会被写入到系统缓冲区中.

该缓冲区的刷新策略为:

  • 立即刷新 (成本较高)- 行刷新 向屏幕打印通常是这个标准
  • 全刷新(满了再刷新) 向文件输入通常是这个标准

进程退出前会刷新C语言的缓冲区.所以当去掉’\n’时,先关闭了stdout,此时未被刷新.所以即使在进程退出时刷新缓冲区,也不会再输出到屏幕上了.

关于系统的缓冲区他有一套自己的规则,我们现在仅需认为,刷新到了系统中的缓冲区,内容就会被写到文件上

有一个fflush接口,是手动刷新该进程的用户缓冲区.

image-20231126132057277

也有一个fclose

image-20231126132143841

我们如果把代码改写成这样

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<unistd.h>intmain(){constchar* buff="hello linux\n";printf("hello io\n");fprintf(stdout,"hello linux i am %d\n",getpid());fclose(stdout);return0;}

image-20231126132244480

结果就会被正确的打印出来

所以 fclose的本质是 fflush+close.刷新了用户层的缓冲区.

d4520d66a5271945285dd39c0375911

FILE*来维护C中的缓冲区空间.所以,每个被打开的文件都有一段属于自己的缓冲区在FILE中存着

最后再来看看这个现象.

#include<stdio.h>#include<string.h>#include<sys/types.h>#include<unistd.h>intmain(){constchar* buff="hello linux\n";
    FILE* f=fopen("./helloio.txt","w");dup2(f->_fileno,1);printf("hello io\n");fprintf(stdout,"hello linux i am %d\n",getpid());write(1,buff,strlen(buff));fork();return0;}

创建了一个子进程,对输出流进行了 重定向,将输出流变为对文件写入.

结果是:

image-20231126133329564

write中的内容被写入了一遍.C接口中被写入两遍

这是因为 进程创建时会复制父进程的所有状态,所以将C语言的缓冲区也拷贝了过来,之前提到过,对普通文件进行写入时,为全刷新,所以即使有’\n’也不会立即刷新缓冲区.而是等待进程退出时再一次刷新.对C缓冲区进行刷新时发生了写时拷贝,父子进程各自拥有一个缓冲区(因为对共享的数据进行了改变),所以被刷新了两次到系统的缓冲区

所以,我们可以大概理解了 C语言的IO接口的底层是什么样的

  1. FILE*内存了fd与描述缓冲区的相关指针
  2. fopen = open + malloc(FILE)
  3. fclose = fflush +close
  4. flush = write
  5. fwrite = write

那么我们就可以自己来实现一个简易版的stdio.h了

5. 实现Stdio.h

Makefile:

test:main.c Mystdio.c
    gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
    rm -rf test

Mystdio.h

#pragmaonce#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<stdlib.h>#include<string.h>#defineSIZE1024#defineMODE0666#defineMALLOC_FAILED2//刷新策略  立即刷新 行刷新 全刷新#defineNOW_FLUSH1#defineLINE_FLUSH2#defineALL_FLUSH4typedefstruct_FILE{int fileno;char buffer[SIZE];int end_pos;int flush_mode;}_FILE;

_FILE*_fopen(constchar* path,constchar* mode);

size_t _fwrite(constvoid*ptr, size_t len, _FILE *stream);int_fclose(_FILE* stream);int_fflush(_FILE* stream);

Mystdio.c:

#pragmaonce#include"Mystdio.h"#include<stdio.h>#include<unistd.h>

_FILE*_fopen(constchar* path,constchar* mode){if(path==NULL)returnNULL;int fd =0;if(strcmp(mode,"w")==0)
        fd =open(path, O_WRONLY | O_CREAT | O_TRUNC ,MODE);elseif(strcmp(mode,"a")==0)
        fd =open(path,O_WRONLY | O_APPEND | O_CREAT, MODE);elseif(strcmp(mode,"r")==0)
        fd =open(path,O_RDONLY );elsereturnNULL;if(fd ==-1)returnNULL;

    _FILE* f =(_FILE*)malloc(sizeof(_FILE));if(f ==NULL){perror("malloc failed: ");exit(MALLOC_FAILED);}

    f->fileno=fd;
    
    f->end_pos=0;//默认刷新为行刷新
    f->flush_mode=LINE_FLUSH;return f;} 

size_t _fwrite(constvoid*ptr, size_t len, _FILE *stream){
    stream->end_pos += len;memcpy(stream->buffer,(char*)ptr,len);if(stream->flush_mode & NOW_FLUSH){write(stream->fileno,stream->buffer,stream->end_pos);
        stream->end_pos=0;}elseif(stream->flush_mode & LINE_FLUSH){if(stream->buffer[stream->end_pos -1]=='\n'){write(stream->fileno,stream->buffer,stream->end_pos);
                stream->end_pos=0;}}elseif(stream->flush_mode & ALL_FLUSH){if(stream->end_pos == SIZE){write(stream->fileno,stream->buffer,stream->end_pos);
                stream->end_pos=0;}}return len;}int_fflush(_FILE* stream){if(stream ==NULL)return-1;if(stream->end_pos >0){write(stream->fileno,stream->buffer,stream->end_pos);
        stream->end_pos=0;}return0;}int_fclose(_FILE* stream){if(stream ==NULL)return-1;_fflush(stream);close(stream->fileno);free(stream);
    stream=NULL;}

image-20230905164632777

标签: linux java 服务器

本文转载自: https://blog.csdn.net/qq_62839589/article/details/134627347
版权归原作者 ppeua 所有, 如有侵权,请联系我们删除。

“【Linux】一文看懂基础IO并模拟实现”的评论:

还没有评论