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

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结构体内,每一个文件都有自己的缓冲区
  •  缓冲区满了会刷新到内核中,而 刷新的本质就是写入

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

相关文章:

Linux-基础IO

&#x1f30e;Linux基础IO 文章目录&#xff1a; Linux基础IO C语言中IO交互       常用C接口         fopen         fputs         fwrite         fgets 当前路径       三个文件流 系统文件IO       open函数     …...

202006青少年软件编程(Python)等级考试试卷(二级)

第 1 题 【单选题】 以下程序的运行结果是&#xff1f;&#xff08; &#xff09; l ["兰溪","金华","武义","永康","磐安","东阳","义乌","浦江"]for s in l:if"义"in s:print(…...

【LeetCode】每日一题:2244.完成所有任务需要的最少轮数

给你一个下标从 0 开始的整数数组 tasks &#xff0c;其中 tasks[i] 表示任务的难度级别。在每一轮中&#xff0c;你可以完成 2 个或者 3 个 相同难度级别 的任务。 返回完成所有任务需要的 最少 轮数&#xff0c;如果无法完成所有任务&#xff0c;返回 -1 。 英文原题&#xf…...

百度文心一言 java 支持流式输出,Springboot+ sse的demo

参考&#xff1a;GitHub - mmciel/wenxin-api-java: 百度文心一言Java库&#xff0c;支持问答和对话&#xff0c;支持流式输出和同步输出。提供SpringBoot调用样例。提供拓展能力。 1、依赖 <dependency> <groupId>com.baidu.aip</groupId> <artifactId…...

59.基于SSM实现的网上花店系统(项目 + 论文)

项目介绍 本站是一个B/S模式系统&#xff0c;网上花店是在MySQL中建立数据表保存信息&#xff0c;运用SSMVue框架和Java语言编写。并按照软件设计开发流程进行设计实现充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SSM的网…...

什么是字节码?

字节码&#xff08;Bytecode&#xff09;是Java虚拟机&#xff08;JVM&#xff09;能够理解和执行的中间代码。Java源代码首先编译成字节码文件&#xff08;扩展名为 .class&#xff09;&#xff0c;而不是直接编译成特定机器的机器码。字节码具有以下特点&#xff1a; 平台无…...

C++ JWT的使用

接入sdk需要使用JWT加密参数&#xff0c;做个记录以备后查 #include <iostream> #include <jwt-cpp/jwt.h> int main() { // 设置JWT的密钥&#xff08;对于HS256&#xff09; std::string secret_key "your-256-bit-secret"; // 创建一个新的JW…...

SpringBoot内置插件的使用(jackson和lombok)

文章目录 引言I lombok(自动为属性生成构造器)II jacksonsee also引言 idea正式版2021.2.2 已经捆绑安装jackson和lombok插件 I lombok(自动为属性生成构造器) Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString方法。 htt…...

Franz Electron + React 源码启动运行填坑指南

环境要求 安装miniconda python 环境electron/rebuild用得着&#xff0c;miniconda 默认自带的 python 是 3.11 版本&#xff0c;比较新&#xff1b; 安装virsual studio 2019 要把C桌面相关的都安装了&#xff0c;大概需要20G&#xff0c;不要安装到 C 盘&#xff0c;都安装到…...

网络安全法中关于网络信息的保护和监管,有哪些规定?

网络安全法作为我们数字时代的重要法律保障&#xff0c;对于网络信息的保护和监管有着明确且详细的规定。这些规定不仅体现了国家对于网络安全的重视&#xff0c;也为我们每个人在数字世界中提供了坚实的法律屏障。 首先&#xff0c;我们来看一个关于网络运营者主体责任的案例。…...

前端XHR请求数据

axios封装了XHR(XMLHttpRequest) 效果 项目结构 Jakarta EE9&#xff0c;Web项目。 无额外的maven依赖 1、Web页面 index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title&…...

利用香港多IP服务器优化网站访问速度的关键策略?

利用香港多IP服务器优化网站访问速度的关键策略? 随着数字化时代的不断发展&#xff0c;网站的全球访问速度成为企业吸引用户、提升竞争力的重要因素。特别对于跨国企业而言&#xff0c;如何确保全球用户都能享受到稳定快速的访问体验显得尤为重要。在这一背景下&#xff0c;…...

如何快速将视频做成二维码?扫描二维码播放视频的制作方法

视频二维码的用途越来越多&#xff0c;比如常见的有产品展示、企业宣传、教程说明、个人展示等都可以生成二维码&#xff0c;通过扫码在手机或者其他设备上预览内容&#xff0c;从而提升其他人获取视频的速度&#xff0c;实现内容的快速分享。 对于有制作视频二维码需求的小伙…...

使用python开发的闭运算调试器

使用python开发的开运算调试器 简介效果代码 简介 用来调试闭运算效果的小工具&#xff0c;滑动条可以控制滤波核的大小&#xff0c;用来查看不同滤波核下的闭运算效果。 效果 代码 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayou…...

一例Phorpiex僵尸网络变种的分析

概述 这是一例Phorpiex僵尸网络变种&#xff0c;通过NSIS打包&#xff0c;加载恶意dll(Flaminius.dll)&#xff0c;读取dat文件&#xff08;Preoral.dat&#xff09;&#xff0c;在内存解密并解压缩出一个Pe&#xff0c;创建同名傀儡进程并注入。通过可移动存储介质传播&#…...

PDF文件转换为CAD的方法

有时候我们收到一个PDF格式的设计图纸&#xff0c;但还需要进行编辑或修改时&#xff0c;就必须先将PDF文件转换回CAD格式。分享两个将PDF转换回CAD的方法&#xff0c;一个用到在线网站&#xff0c;一个用到PC软件&#xff0c;大家根据情况选择就可以了。 ☞在线CAD网站转换 …...

Java为什么会成为现在主流的编程语言

Java为什么会成为现在的主流语言 前言一、Java语言概述Java是什么为什么大多数人会选择从事Java为什么从事Java的工作者数量从年递减 二、Java语言的特点简单性面向对象分布式&#xff08;微服务&#xff09;健壮性安全性体系结构中立可移植性解释型高性能多线程动态性 三、Jav…...

动手学深度学习16 Pytorch神经网络基础

动手学深度学习16 Pytorch神经网络基础 1. 模型构造2. 参数管理1. state_dict()2. normal_() zeros_()3. xavier初始化共享参数的好处 3. 自定义层4. 读写文件net.eval() 评估模式 QA 1. 模型构造 定义隐藏层–模型结构定义前向函数–模型结构的调用 import torch from torch…...

前端无样式id或者class等来定位标签

目录&#xff1a; 1、使用背景2、代码处理 1、使用背景 客户使用我们产品组件&#xff0c;发现替换文件&#xff0c;每次替换都会新增如下的样式&#xff0c;造就样式错乱&#xff0c;是组件的文件&#xff0c;目前临时处理的话就是替换文件时删除新增的样式&#xff0c;但是发…...

机器人工具箱学习(三)

一、动力学方程 机器人的动力学公式描述如下&#xff1a; 式中&#xff0c; τ \boldsymbol{\tau} τ表示关节驱动力矩矢量&#xff1b; q , q ˙ , q \boldsymbol{q} ,\; \dot{\boldsymbol { q }} ,\; \ddot{\boldsymbol { q }} q,q˙​,q​分别为广义的关节位置、速度和加速…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?

在建筑行业&#xff0c;项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升&#xff0c;传统的管理模式已经难以满足现代工程的需求。过去&#xff0c;许多企业依赖手工记录、口头沟通和分散的信息管理&#xff0c;导致效率低下、成本失控、风险频发。例如&#…...

JVM垃圾回收机制全解析

Java虚拟机&#xff08;JVM&#xff09;中的垃圾收集器&#xff08;Garbage Collector&#xff0c;简称GC&#xff09;是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象&#xff0c;从而释放内存空间&#xff0c;避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

Mac下Android Studio扫描根目录卡死问题记录

环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中&#xff0c;提示一个依赖外部头文件的cpp源文件需要同步&#xff0c;点…...

Linux离线(zip方式)安装docker

目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1&#xff1a;修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本&#xff1a;CentOS 7 64位 内核版本&#xff1a;3.10.0 相关命令&#xff1a; uname -rcat /etc/os-rele…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

为什么要创建 Vue 实例

核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...