Linux-基础IO

🌎Linux基础IO


文章目录:

Linux基础IO

    C语言中IO交互
      常用C接口
        fopen
        fputs
        fwrite
        fgets

      当前路径
      三个文件流

    系统文件IO
      open函数
      参数含义
      close函数

      write函数
        参数含义

    文件描述符fd
      认识文件描述符

      重定向
        输出重定向
        输入重定向
        追加重定向
        重定向接口

    缓冲区
      简单认识缓冲区
      技术角度认识缓冲区
      FILE结构体
      编码模拟

    总结


前言:

  在刚开始学习Linux的时候,我们记住了Linux下一切皆文件,我们通过这篇文章来深入了解Linux下文件的构成及应用。

在这里插入图片描述


🚀C语言中IO交互

✈️ 常用C接口
🚩 fopen

fopen:打开一个文件。

代码示例:

#include<stdio.h>int main()
{FILE* fp = fopen("./log.txt", "w");//打开一个文件,如果没有则创建一个文件if(fp == NULL){perror("fopen");return 1;}//文件操作介于打开和关闭之间fclose(fp);//关闭文件return 0;
}

在这里插入图片描述

注意

  当以 ‘w’ 方式打开文件时:该文件会被清空。

  当以 ‘a’ 方式打开文件时:正常打开该文件,如果有写入操作则是追加写入。

  当以 ‘r’ 方式打开文件时:仅读取文件。


🚩 fputs

fputs:向文件流中写入一个字符串

代码示例:

#include<stdio.h>int main()
{FILE* fp = fopen("./log.txt", "w");if(fp == NULL){perror("fopen");return 1;}const char* str = "this is file operate\n";fputs(str, fp);fclose(fp);return 0;
}

在这里插入图片描述


🚩 fwrite

fwrite:向二进制文件写入数据。

代码示例:

#include<stdio.h>
#include<string.h>#define FILENAME "log.txt"int main()
{FILE* fp = fopen("./log.txt", "w");if(fp == NULL){perror("fopen");return 1;}const char* msg = "this is file operate\n";int cnt = 5;while(cnt){fwrite(msg, strlen(msg), 1, fp);printf("write %d block\n", n);cnt--;}fclose(fp);return 0;
}

在这里插入图片描述

第一个参数:

写入数据的对象。

第二个参数:

基本单位的大小。

第三个参数:

表示写入多少个基本单位。

第四个参数:

表示文件流

返回值:

表示写入的基本单位的个数,也就是第三个参数

在这里插入图片描述


🚩 fgets

fgets:读取一个字符串。

代码示例:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>int main()
{FILE* fp = fopen("./log.txt", "r");//r方式打开if(fp == NULL){perror("fopen");return 1;}char buffer[64];while(1){char* r = fgets(buffer, sizeof(buffer), fp);if(!r) break;printf("%s\n", buffer);}fclose(fp);return 0;
}

在这里插入图片描述

  这里我只列举了部分常用C语言IO接口,如果有遗忘,请自行复习。


✈️当前路径

  当我们在程序中创建一个文件时,例如使用 fopen函数以 ‘w’ 方式打开文件,文件不存在时则创建文件,但是为什么文件创建位置是在当前路径下呢?

在这里插入图片描述
  其实是通过该进程的一项属性数据来判断所处路径的,我们可以查询该进程pid,在proc目录下进行查看该进程:

在这里插入图片描述

  cwd表示该进程当前所处工作目录,exe表示可执行程序所处路径。

注意: 当前路径不是指可执行程序所处路径,而是指该程序运行为进程时所处路径。


✈️三个文件流

  刚开始接触Linux的时候,我们都知道有句话叫做:Linux下一切皆文件,那么键盘、显示器、网卡、声卡等等这些对于Linux来说都是文件!

  我们使用Linux都知道,想要对一个文件进行操作,我们必须要打开一个文件,这是必须的。但是为什么 显示器文件键盘文件 这些文件我们并不需要直接打开就可以直接使用呢?

文件在打开的前提一定是基于进程的,而进程在运行的过程中会打开默认的三个流,即标准输入流,标准输出流、标准错误流。而对应C语言中就是 stdin、stdout、stderr

  标准输入流对应的设备是键盘、标准输出与标准错误流对应的设备是显示器。

在这里插入图片描述
  当我们使用C语言运行一个程序的时候,操作系统会默认将这三个流给打开,于是,我们使用printf、scanf、gets、puts等接口时可以直接使用。

  也就是说我们的输入输出是因为stdin和stdout流是默认打开的状态,我们可以根据stdin、stdout来直接对屏幕进行输出:

#include<stdio.h>int main()
{fprintf(stdout, "you can see me\n");//对标准输出流进行写入fprintf(stdout, "yes I'can\n");//对标准输出流进行写入return 0;
}

在这里插入图片描述
  对标准输出流进行写入,其实就是将数据打印到显示器上!

注意:并不是只有C语言有此特性,其他语言例如C++的cout、cin也具有标准流。这种特性并不是有语言层面提供的,而是由操作系统提供的。


🚀系统文件IO

  除了使用C语言或者其他语言的IO交互,我们也可以采用调用系统接口来进行文件访问,而系统调用时更接近于底层的,其他语言都是对系统的系统调用进行封装的。

✈️open函数

open函数是fopen函数的底层,其为Linux的系统调用,函数原型为:

int open(const char *pathname, int flags, mode_t mode);
参数含义
  • pathname:表示 需要传入的文件路径,当只有文件名的时候,表示子在当前目录打开或创建该文件。

  • flags:表示打开文件的方式。通常打开文件的常用方式分为以下几种:

flags选项含义
O_RDONLY以只读的方式打开文件
O_WRONLY以只写的方式打开文件
O_APPEND以追加的方式打开文件
O_CREAT文件不存在时,则创建文件
O_RDWR以读写的方式打开文件
O_TRUNC清空文件
  • mode:表示创建文件的默认方式。不需要创建文件时,这个参数不必传参。

  为了能理解第二个参数flags ,我们通过以下代码来观察:

#include<stdio.h>#define O_LISTEN 1// 0001
#define O_TALK 2 // 0010
#define O_READ 4  // 0100
#define O_WRITE 8 // 1000void Listen()
{printf("linten English dialog\n");
}void Talk()
{printf("talk about English\n");
}void Read()
{printf("read English newspaper\n");
}void Write()
{printf("write English article\n");
}void operate(int flags)
{//根据二进制位来判断调用函数接口类型if(flags & O_LISTEN)Listen();if(flags & O_TALK)Talk();if(flags & O_READ)Read();if(flags & O_WRITE)Write();
}int main()
{operate(O_LISTEN);printf("\n");operate(O_TALK | O_READ);//按位或运算调用printf("\n");operate(O_LISTEN | O_TALK | O_READ | O_WRITE);return 0;
}

在这里插入图片描述

  我们可以 使用或运算 来做出 不同的行为,同样,open接口的flags参数也是如此使用方式,例如,我们以 使用open模拟fopen函数的 ‘w’ 行为

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);//文件默认权限设置为666if(fd == -1){perror("open");}return 0;
}

在这里插入图片描述

  我们确实模仿出了fopen函数的功能,仔细看文件权限,与我们想要的并不同,最后三项应该是 rw- 才对,这是因为存在叫做 权限掩码(umask) 的东西,其通常默认为0002,与mode的关系是 umask & mode,所以我们在设置权限之前,需要把umask设置为0:

在这里插入图片描述

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{umask(0);int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);//文件默认权限设置为666if(fd == -1){perror("open");}return 0;
}

在这里插入图片描述

✈️close函数
int close(int fd);

  close函数属于Linux下的系统调用,其功能是 关闭一个文件描述符,参数是 有待关闭的文件描述符。

在这里插入图片描述

✈️write函数

函数定义

ssize_t write(int fd, const void* buf, size_t count);
🚩 参数含义
  • fd:需要传入的文件描述符。
  • buf:需要写入的字符串的起始位置。
  • count:需要写入字符串的长度。

  其中第三个参数需要注意,传入的字符串长度是不算 \0 的,因为这是系统调用接口,并非C语言。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{umask(0);int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);if(fd == -1){perror("open");return 1;}const char* str = "hello sys call\n";write(fd, str, strlen(str));//长度不算 \0close(fd);return 0;
}

在这里插入图片描述

  但是如果我们写入的字符串改变了并且没有 \n:

const char* str = "aaaa";
write(fd, str, strlen(str));

在这里插入图片描述
  如果这样,那么下次进行写入就是以 覆盖的方式进行写入。所以我们在打开文件的时候需要将open函数的选项增加一个 O_TRUNC 选项:

int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

在这里插入图片描述

  如果需要实现什么功能,就需要提供对应的选项。


🚀文件描述符fd

  文件描述符在上文中不止出现了一次,包括 open 函数的返回值,close 函数的参数等等,从其出现的频率来看,似乎是很重要的一个东西。

✈️认识文件描述符

  既然open 函数返回值是文件描述符,那么我们可以创建多个open函数,使用多个返回值接收并且打印来观察现象:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);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;
}

在这里插入图片描述

  我们观察到的现象是,文件描述符是从3开始的,那么012去哪里了?并且为什么它们是连续的??

  其实0、1、2文件描述符已经被使用了!其分别是:标准输入、标准输出、标准错误!而它们是连续的,其实也就是 数组下标

  而我们在上文中也提到过三个标准流,即:

在这里插入图片描述

  他们的类型都是 FILE* 类型,其实 FILE 是C标准库自己封装的一个结构体。而这三个流分别是文件描述符的前三个,那么 FILE 结构体内必定 封装特定的fd!

  我们经常说,Linux下一切皆文件,那么一个空文件,它的大小真的是0吗,其实在很久以前我们也探讨过,只要文件被创建,那么就不可能为0。

  文件 = 内容 + 属性

  那么每个文件必然具有一些相同的初始属性,比如文件标志位,文件权限位,文件对下一个文件的指针,缓冲区等等。这些属性很杂乱,所以操作系统需要对其进行管理,那么还是那六个字:先描述,再组织

  将这些属性组织到结构体当中,便更有利于操作系统的管理:

在这里插入图片描述

  在task_struct 中存在一个 files 指针,该指针指向一个 files_struct 的结构体,在该结构体当中存在一个 fd_array 的指针数组,而 数组的下标就对应我们所谓的文件描述符

在这里插入图片描述

  因为0、1、2这三个文件描述符时默认打开的,但是这里我把它关闭(仅关闭0位置),再使用 open 创建一个文件,会发生什么?

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{close(0);int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);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;
}

在这里插入图片描述
  0位置的fd被我们关闭了,但当我们在创建文件的时候,0号位置被新创建的文件占用了。如果我再关闭2号文件描述符呢?

close(0);
close(2);

在这里插入图片描述

  看来我们 关闭 一个默认打开的文件描述符,那么新建文件就会:按照顺序占用 被关闭(未被使用) 的文件描述符。


✈️重定向

  了解了什么是文件描述符之后,我们就可以根据文件描述符的规则来实现不同的重定向功能。

  我们在最开始学习Linux指令的时候使用过重定向功能,而重定向无外乎 输入重定向输出重定向

在这里插入图片描述

  重定向的原理是,将原本需要输入或者输出的对象文件变为指定的对象文件

  比如,我们知道Linux下一些皆文件,那么键盘、显示器都是文件,而我们平常的打印,其实就是对 “显示器文件” 上进行写入,而重定向就是将原本向 “显示器文件” 写入更改为向其他文件写入。

🚩 输出重定向

  而更改重定向文件其实是就是更改文件描述符指向的文件:

在这里插入图片描述

我们使用C语言来模拟一下情况:

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

在这里插入图片描述


🚩 输入重定向

  同样,输入重定向也是先关闭默认打开的0号文件描述符,使得新创建的分配到0号文件描述符,这样进行输入的时候就重定向到该文件内:

在这里插入图片描述

C语言模拟:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{close(0);umask(0);int fd = open("log1.txt", O_RDONLY|O_CREAT, 0666);//这里为只读if(fd < 0){perror("open");return 1;}char buff[64];while(scanf("%s", buff) == EOF){printf("%s\n", buff);}close(fd);return 0;
}

在这里插入图片描述


🚩 追加重定向

  追加重定向,与输出重定向不同的是,输出重定向每次向文件内输入时都会清空文件内容再做输入,而追加重定向是追加写入文件内,不修改原来文件的文本。

  其实实现起来也很简单,将open 函数的flags参数添加上 O_APPEND 即可:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{close(1);umask(0);int fd = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);if(fd < 0){perror("open");return 1;}printf("Hello Linux xxxxxxxxxxxx\n");printf("Hello Linux xxxxxxxxxxxx\n");printf("Hello Linux xxxxxxxxxxxx\n");printf("Hello Linux xxxxxxxxxxxx\n");fflush(stdout);close(fd);return 0;
}

在这里插入图片描述


🚩 重定向接口

  我们整个重定向需要搞那么麻烦吗?万一在代码段当中添加了其他需求到最后自己是否还能认得这段代码?为了方便,Linux给我们提供了一个接口,dup2

在这里插入图片描述

直接一段代码来看用法:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>int main()
{umask(0);int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);dup2(fd, 1);//将fd重定向到1printf("Can you see me??\n");printf("Can you see me??\n");printf("Can you see me??\n");printf("Can you see me??\n");close(fd);return 0;
}

在这里插入图片描述

  追加与输入重定向皆可使用dup接口进行重定向,这样简化了代码量,使代码更具可读性。


🚀缓冲区

✈️简单认识缓冲区

  我们可能经常听到 “缓冲区” 这个词,它到底是什么或许你还没有深究过,缓冲区本质上就是一块内存区域,那么为什么要有缓冲区呢?我们来看下面的例子:

  他穿越了,穿越到了千禧年的大学生身上,有一个高中同学叫做阿飞,阿熊在安徽上大学,阿飞在广东上大学。过两天就是阿飞的生日,阿熊买了一个最新显卡准备坐火车送给阿飞,于是阿熊买了火车票,一路颠簸的去掉了广东,然后把礼物送给阿飞,吃顿饭就走了。
在这里插入图片描述
  阿熊回到了2024年,正巧阿熊现在的高中同学阿乐也在广东准备过生日,阿熊在安徽,于是阿熊精心挑选了一个键盘,准备送给阿乐。现在是2024年,阿熊拿上键盘直接下楼到邮政快递公司把快递寄了过去。
  但是快递公司并不是拿到你的快递就开始出发配送,而是要等到一定的量到了再配送。等到一车货够了,那么就会出发送快递。过了两天阿乐收到了你的消息,于是也下楼到邮政取了快递。

  上述情况,我们仅仅是为了送一个生日礼物,但是这样的开销是不是就太大了,不仅要买来回车票,到地方可能还要住旅馆,而且花费的时间也很多。这就好比操作系统把每一次的输入都立即送到显示器上一样,电信号看似很快,但是千千万万个信息呢?

  而有了快递公司就方便了许多,只需要下楼寄个快递,等到一定数目的快递集齐了快递就可以发过去了,而对方收到快递也仅仅只需要下楼到快递公司取个快递。

  不论是C语言,还是操作系统,它们同样如此,既然一次一次来回写入开销很大,倒不如开辟一块内存区域,当内容空间的内容满了,再做刷新。

  所以,总的来说,缓冲区其实就是 以空间换时间的一种方式


✈️技术角度认识缓冲区

  我们以前所接触的缓冲区几乎都是语言层面的缓冲区,而缓冲区也分为系统层和语言层缓冲区。

在这里插入图片描述

  C语言中的printf/fgets等函数底层其实就是调用系统调用来实现输出的。但是系统调用本身就是需要成本的,所以我们用户层面就要尽量较少的访问系统调用。

  这就好比,阿熊月末没钱了,通常一顿饭要10块钱,撑到下个月大概还有不到10顿,那么阿熊是向朋友一次借10块分10次借还是一次借100就借一次呢?显然阿熊会选择后者。

  C语言也是这么想的,所以C原也提供了缓冲区,我们通常写入数据其实 写入的是C语言的缓冲区,再由C语言调用系统调用把数据刷新到内核当中。从而间接减少系统调用的次数。

缓冲类型分为:

  • 全缓冲:全部刷新,普通文件缓冲区写满才刷新。
  • 行刷新:\n之前的内容进行刷新。
  • 无刷新:无刷新。

✈️FILE结构体

  既然存在缓冲区这个东西,那么它存储在哪呢?实际上 缓冲区是由FILE结构体来维护的

  在上文我们说stdin、stdout、stderr这三个流的类型皆是 FILE* 类型,而每个文件都有自己的FILE结构体,所以 每个文件都有自己的缓冲区

  不仅如此,C语言的很多接口的参数也都是FILE* 类型:

在这里插入图片描述

   拿fwrite来举例,仅仅是把 *ptr 的 (size * nmemb) 字节大小的内容拷贝到 FILE 缓冲区内,需要的时候内部再决定如何刷新。

  所以这些接口大部分时间都是向FILE内的缓冲区进行拷贝,所以在 用户层面上这些接口的效率也比较高

我们来看看C语言库是如何定义的:

/usr/include/libio.h
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 0int _blksize;
#elseint _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
};

  由此可以清晰的观察到C语言级别的缓冲区是如何定义的,这里需要注意的另一个点是 文件描述符被 _fileno 封装

  下面我写一段代码来证明缓冲区的存在:

#include<stdio.h>
#include<unistd.h>
#include<string.h>int main()
{//Use system callconst char* s1 = "hello write\n";write(1, s1, strlen(s1));//Use C intefaceconst char* s2 = "hello fprintf\n";fprintf(stdout, "%s", s2);const char* s3 = "hello  fwrite\n";fwrite(s3, strlen(s3), 1, stdout);fork();//注意这里进行forkreturn 0;
}

在这里插入图片描述
  这个现象就很有趣了,第一次运行没什么问题,三个数据全部打印出来,但是当我们第二次运行并且重定向到空文件当中时却出了问题,你可以先思考为什么。

  其实这是因为,第一次运行程序其实是向显示器打印,这个行为默认的刷新行为是 行刷新。而第二次重定向到了文件中,这个时候刷新方式就变为了 全缓冲
  而全缓冲正常情况下是进程退出时才进行刷新策略的。而在程序的最后我们进行了fork创建了子进程。
  而这个时候,缓冲区接收的数据没有满,所以这个时候不论哪个进程先退出,都会将数据写入到C语言中的缓冲区当中,最终造成了打印出来的数据有两项是重复的。
  而write为什么只打印一次?这是因为write函数是系统调用,并 不参与 语言层的缓冲区,所以只打印一次。

在这里插入图片描述

  当某一个进程退出时,那么一定要将自己缓冲区中的数据刷新到内核当中,而 刷新的本质就是写入!而一旦写入就会 立马发生 写时拷贝,子进程就有自己的缓冲区,将数据写入到缓冲区中,子进程退出后就会造成二次刷新。

  而这个现象也恰恰说明了语言层是存在缓冲区的。


✈️编码模拟

  为了更加深刻理解缓冲区这个概念,我们不妨编写一段代码来加深印象:

bash准备:

[xzy@iZ0jle4p97d8x4byf3u32mZ buffer]$ ll
total 0
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 filetest.c
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 makefile
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 mystdio.c
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 mystdio.h

mystdio.h:

#pragma once #include<stdio.h>#define SIZE 4096
#define NONE_FLUSH (1<<1)//无刷新
#define LINE_FLUSH (1<<2)//行刷新
#define FULL_FLUSH (1<<3)//全缓冲typedef struct _myFILE
{char outbuffer[SIZE];//输出缓冲区int pos;//位置int cap;//容量int fileno;//文件描述符int flush_mode;//刷新方式
}myFILE;myFILE *my_fopen(const char* pathname, const char* mode);void my_fclose(myFILE* fp);int my_fwrite(myFILE* fp, const char* s, int size);

mystdio.c:

#include "mystdio.h"
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>const char* toString(int flag)
{if(flag & NONE_FLUSH) return "None";//无缓冲else if(flag & LINE_FLUSH) return "Line";//flag为行缓冲else if(flag & FULL_FLUSH) return "FULL";//全缓冲return "Unknow";
}void DebugPrint(myFILE* fp)//debug代码是否有误
{printf("outbuffer: %s\n", fp->outbuffer);printf("fd: %d\n", fp->fileno);printf("pos: %d\n", fp->pos);printf("cap: %d\n", fp->cap);printf("flush_mode: %s", toString(fp->flush_mode));
}myFILE* my_fopen(const char* pathname, const char* mode)//模拟fopen函数
{int flag = 0;if(strcmp(mode, "r") == 0)//r方式打开{flag |= O_RDONLY;}else if(strcmp(mode, "w") == 0)//w 方式打开{flag |= (O_CREAT | O_WRONLY | O_TRUNC);}else if(strcmp(mode, "a") == 0)//a 方式打开{flag |= (O_CREAT | O_WRONLY | O_APPEND);}else {return NULL;}int fd = 0;if(flag & O_WRONLY)//是否为写的方式打开{umask(0);fd = open(pathname, flag, 0666);//写的方式打开很可能会创建文件}else {fd = open(pathname, flag);//只读方式打开}if(fd < 0) return NULL;//将FILE对象初始化myFILE* fp = (myFILE*)malloc(sizeof(myFILE));fp -> fileno = fd;fp -> cap = SIZE;fp -> pos = 0;fp -> flush_mode = LINE_FLUSH;//默认为行缓冲return fp;
}void my_fflush(myFILE* fp)//刷新
{if(fp->pos == 0) return;write(fp->fileno, fp->outbuffer, fp->pos);fp->pos = 0;
}void my_fclose(myFILE* fp)//自定义关闭文件
{my_fflush(fp);//退出前要刷新close(fp -> fileno);free(fp);
}int my_fwrite(myFILE* fp, const char* s, int size)//自定义fwrite
{memcpy(fp->outbuffer + fp->pos, s, size);fp->pos += size;if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos - 1] == '\n') //行刷新{my_fflush(fp);   }else if((fp->flush_mode & FULL_FLUSH) && fp->pos == fp->cap)//全缓冲{my_fflush(fp);}return size;
}

filetest.c:

#include"mystdio.h"
#include<string.h>const char* filename = "./log.txt";//文件名称int main()
{myFILE* fp = my_fopen(filename, "w");//以写的方式打开文件if(fp == NULL) return 1;int cnt = 5;//进行数量测试char buffer[64];//缓冲区while(cnt){snprintf(buffer, sizeof(buffer), "youcanseeme, bro:%d \n", cnt--);my_fwrite(fp, buffer, strlen(buffer));sleep(2);}my_fclose(fp);return 0;
}

在这里插入图片描述

  运行成功之后,我们就可以看到现象,在log文件中打印了我们测试的内容。


📒✏️总结

  •  C语言的一些IO接口需要熟悉,例如fwrite,fputs等等。
  •  当前当前路径是根据进程的cwd来决定的,C语言默认打开三个流:stdin、stdout、stderr。他们三个 分别占用0、1、2三个文件描述符
  •  系统层面的IO交互接口有 write、open、close、read等需要理解。
  • 文件=内容+属性;一个文件是否为空都会存在属性,而操作系统为了维护文件的属性,先描述再组织,将文件的属性组织为一个结构体file,而 每个file以双链表的形式相连
  •  因为Linux下一切皆文件,所以文件也需要被组织起来,于是file结构体的指针file*被组织起来封装在一个叫做files_struct 指针数组内,而数组下标就是 文件描述符
  •  重定向是 根据更改文件描述符的指向文件 做到的,可以使用dup2接口做调整。
  •  缓冲区本质上是一块内存区域,而缓冲区分为系统层缓冲区和语言层缓冲区,在C语言中缓冲区被封装在FILE结构体内,每一个文件都有自己的缓冲区
  •  缓冲区满了会刷新到内核中,而 刷新的本质就是写入

在这里插入图片描述
  希望这篇文章能够帮到你【玫瑰】~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/837858.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

特征模态分解(FMD):一种小众而又新颖的分解方法

​ 声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 今天为大家介绍一个小众而又新颖的信号分…

tensorflow实现二分类

# 导入所需库和模块 from tensorflow.keras.layers import Dense, Input, Activation # 导入神经网络层和激活函数模块 from tensorflow.keras.models import Sequential # 导入Keras的Sequential模型 import pandas as pd # 导入Pandas库用于数据处理 import numpy as np …

接口文档不显示新写的接口

新写的接口&#xff0c;但是不显示&#xff1a; 仔细对比源码才发现没有写tag&#xff1a; 然后就有了&#xff1a;

ES6之正则扩展

正则表达式扩展 u修饰符&#xff08;Unicode模式&#xff09;y修饰符&#xff08;Sticky或粘连模式&#xff09;s修饰符&#xff08;dotAll模式&#xff09;Unicode属性转义正则实例的flags属性字符串方法与正则表达式的整合 javascript的常用的正则表达式 验证数字邮箱验证手机…

C语言中的循环队列与栈、队列之间的转换实现

引言 在数据结构的学习中&#xff0c;栈&#xff08;Stack&#xff09;和队列&#xff08;Queue&#xff09;是两个非常重要的概念。它们分别遵循着后进先出&#xff08;LIFO&#xff09;和先进先出&#xff08;FIFO&#xff09;的原则。在某些情况下&#xff0c;我们可能需要…

C++——超简单登录项目

程序入口文件 #include <QtWidgets/QApplication> // 包含登录页面头文件 #include "DlgLogin.h"int main(int argc, char *argv[]) {QApplication a(argc, argv);// 程序入口// 调页面起来//DlgMain w;//w.show();// 换成登录页面DlgLogin w;w.show();return…

开源禅道zentao的使用

很不幸禅道因为漏洞被人进攻了&#xff0c;被迫研究。 1.安装 直接使用docker进行部署&#xff0c;这里有非常多门道。官网的镜像easysoft-zentao是属于docker安装&#xff0c;而idoop的镜像虽然也是docker安装&#xff0c;但是实际是使用官网linux一键安装的版本&#xff0c…

一周学习总结:数组与链表

学习内容&#xff1a;数组与链表、计算机网络知识 数组&#xff1a; 从数组的基础知识到相关应用 数组的基础知识&#xff1a;数组在内存中的存储、数组的相关操作&#xff08;获取与更新&#xff09;、数组的相关应用&#xff1a; 二分查找法⭐⭐⭐⭐⭐ ● 掌握左闭右闭的…

2024第16届四川教育后勤装备展6月1日举办 欢迎参观

2024第16届四川教育后勤装备展6月1日举办 欢迎参观 邀请函 主办单位&#xff1a; 中国西部教体融合博览会组委会 承办单位&#xff1a;重庆港华展览有限公司 博览会主题&#xff1a;责任教育 科教兴邦 组委会&#xff1a;交易会159交易会2351交易会9466 展会背景 成都…

Chatgpt教你使用Python开发iPhone风格计算器

上次使用Chatgpt写爬虫&#xff0c;虽然写出来的代码很多需要修改后才能运行&#xff0c;但Chatgpt提供的思路和框架都是没问题。 这次让Chatgpt写一写GUI程序&#xff0c;也就是你常看到的桌面图形程序。 由于第一次测试&#xff0c;就来个简单点的&#xff0c;用Python写用…

GPU Burn测试指导

工具下载链接&#xff1a; https://codeload.github.com/wilicc/gpu-burn/zip/master测试方法&#xff1a; 上传工具到操作系统下&#xff0c;解压缩工具&#xff0c;使用make命令完成编译&#xff08;确保cuda环境变量已经配置成功、 nvcc -v能显示结果&#xff09;。 如果安…

文献速递:多模态深度学习在医疗中的应用--多模式婴儿脑分割技术:模糊引导深度学习

Title 题目 Multimodal Infant Brain Segmentation by Fuzzy-informed Deep Learning 多模式婴儿脑分割技术&#xff1a;模糊引导深度学习 01 文献速递介绍 日益普及的非侵入式婴儿脑磁共振图像&#xff08;MRI&#xff09;为准确理解脑主要发展轨迹的动态性提供了机会&…

树莓派|串口通信协议

1、串口通信原理 串口通讯(Serial Communication)&#xff0c;是指外设和计算机间&#xff0c;通过数据信号线、地线等&#xff0c;按位进行传输数据的一种通讯方式。串口是一种接口标准&#xff0c;它规定了接口的电气标准&#xff0c;没有规定接口插件电缆以及使用的协议。串…

“ModuleNotFoundError: No module named ‘selenium‘”报错如何解决

接上节&#xff1a;测试平台开发之测试框架改造并发执行及结果隔离(1) 上节博客的末尾提到&#xff1a;在命令窗口执行python main.py 可是执行的时候遇到了如下报错&#xff1a; ERRORS _____________________________________________________________ ERROR collecting te…

如何安全高效地进行4S店文件分发,保护核心资产?

4S店与总部之间的文件分发是确保双方沟通顺畅、信息共享和决策支持的重要环节。4S店文件分发涉及到以下文件类型&#xff1a; 销售报告&#xff1a;4S店需要定期向总部提交销售报告&#xff0c;包括销售数量、销售额、市场份额等关键指标。 库存管理文件&#xff1a;包括车辆库…

使用docker创建hadoop集群:Couldn‘t upload the file

运行的环境; Windows10 Docker Desktopdocker-hadoop 出现的问题如下: 解决方法 https://github.com/big-data-europe/docker-hadoop/issues/98

Unity Pixels Per Unit 与 Sprite Renderer Scale的逻辑关系,为什么平铺的Sprite Renderer会变形?

SpriteRenderer之前用的比较基础&#xff0c;没遇到过什么问题&#xff0c;这几天使用SpriteRenderer的平铺时发现平铺变形了&#xff0c;研究了一下&#xff0c;原来有这么多在逻辑在里面。 当我们导入图片选择Texture Type为Sprite时表示我们的图片用途是UI或者SpriteRendere…

【go项目01_学习记录12】

代码组织 1 代码结构2 重构与测试2.1 安装测试功能2.2 testify 的常用断言函数 3 表组测试 1 代码结构 所有的代码写在一个main.go文件里面&#xff0c;GO编译器也是可以正常执行的。但是当代码量很庞大时&#xff0c;很难进行维护。 Go Web 程序的代码组织 单文件——反模式…

C语言笔记15

指针2 1.数组名的理解 int arr[ 10 ] { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int *p &arr[ 0 ];17391692786 arr是数组名&#xff0c;数组名是首元素地址&#xff0c;&arr[0]就是取出首元素的地址放在指针变量p中。 #include <stdio.h> int main()…

基于GWO灰狼优化的CNN-GRU-Attention的时间序列回归预测matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1卷积神经网络&#xff08;CNN&#xff09;在时间序列中的应用 4.2 GRU网络 4.3 注意力机制&#xff08;Attention&#xff09; 4.4 GWO优化 5.算法完整程序工程 1.算法运行效果图预览…