🌎Linux基础IO
文章目录:
前言:
在刚开始学习Linux的时候,我们记住了Linux下一切皆文件,我们通过这篇文章来深入了解Linux下文件的构成及应用。
🚀C语言中IO交互
✈️ 常用C接口
🚩 fopen
fopen:打开一个文件。
代码示例:
c
#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:向文件流中写入一个字符串
代码示例:
c
#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:向二进制文件写入数据。
代码示例:
c
#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:读取一个字符串。
代码示例:
c
#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来直接对屏幕进行输出:
c
#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的系统调用,函数原型为:
c
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 ,我们通过以下代码来观察:
c
#include<stdio.h>
#define O_LISTEN 1// 0001
#define O_TALK 2 // 0010
#define O_READ 4 // 0100
#define O_WRITE 8 // 1000
void 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' 行为:
c
#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);//文件默认权限设置为666
if(fd == -1)
{
perror("open");
}
return 0;
}
我们确实模仿出了fopen函数的功能,仔细看文件权限,与我们想要的并不同,最后三项应该是 rw- 才对,这是因为存在叫做 权限掩码(umask) 的东西,其通常默认为0002,与mode的关系是 umask & mode,所以我们在设置权限之前,需要把umask设置为0:
c
#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);//文件默认权限设置为666
if(fd == -1)
{
perror("open");
}
return 0;
}
✈️close函数
c
int close(int fd);
close函数属于Linux下的系统调用,其功能是 关闭一个文件描述符,参数是 有待关闭的文件描述符。
✈️write函数
函数定义:
c
ssize_t write(int fd, const void* buf, size_t count);
🚩 参数含义
- fd:需要传入的文件描述符。
- buf:需要写入的字符串的起始位置。
- count:需要写入字符串的长度。
其中第三个参数需要注意,传入的字符串长度是不算 \0 的,因为这是系统调用接口,并非C语言。
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));//长度不算 \0
close(fd);
return 0;
}
但是如果我们写入的字符串改变了并且没有 \n:
c
const char* str = "aaaa";
write(fd, str, strlen(str));
如果这样,那么下次进行写入就是以 覆盖的方式进行写入 。所以我们在打开文件的时候需要将open函数的选项增加一个 O_TRUNC 选项:
c
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
如果需要实现什么功能,就需要提供对应的选项。
🚀文件描述符fd
文件描述符在上文中不止出现了一次,包括 open 函数的返回值,close 函数的参数等等,从其出现的频率来看,似乎是很重要的一个东西。
✈️认识文件描述符
既然open 函数返回值是文件描述符,那么我们可以创建多个open函数,使用多个返回值接收并且打印来观察现象:
c
#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 创建一个文件,会发生什么?
c
#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号文件描述符呢?
c
close(0);
close(2);
看来我们 关闭 一个默认打开的文件描述符,那么新建文件就会:按照顺序占用 被关闭(未被使用) 的文件描述符。
✈️重定向
了解了什么是文件描述符之后,我们就可以根据文件描述符的规则来实现不同的重定向功能。
我们在最开始学习Linux指令的时候使用过重定向功能,而重定向无外乎 输入重定向 和 输出重定向。
重定向的原理是,将原本需要输入或者输出的对象文件变为指定的对象文件。
比如,我们知道Linux下一些皆文件,那么键盘、显示器都是文件,而我们平常的打印,其实就是对 "显示器文件" 上进行写入,而重定向就是将原本向 "显示器文件" 写入更改为向其他文件写入。
🚩 输出重定向
而更改重定向文件其实是就是更改文件描述符指向的文件:
我们使用C语言来模拟一下情况:
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语言模拟:
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 即可:
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|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:
直接一段代码来看用法:
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()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
dup2(fd, 1);//将fd重定向到1
printf("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语言库是如何定义的:
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 0
int _blksize;
#else
int _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 封装。
下面我写一段代码来证明缓冲区的存在:
c
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
//Use system call
const char* s1 = "hello write\n";
write(1, s1, strlen(s1));
//Use C inteface
const char* s2 = "hello fprintf\n";
fprintf(stdout, "%s", s2);
const char* s3 = "hello fwrite\n";
fwrite(s3, strlen(s3), 1, stdout);
fork();//注意这里进行fork
return 0;
}
这个现象就很有趣了,第一次运行没什么问题,三个数据全部打印出来,但是当我们第二次运行并且重定向到空文件当中时却出了问题,你可以先思考为什么。
其实这是因为,第一次运行程序其实是向显示器打印,这个行为默认的刷新行为是 行刷新 。而第二次重定向到了文件中,这个时候刷新方式就变为了 全缓冲 !
而全缓冲正常情况下是进程退出时才进行刷新策略的。而在程序的最后我们进行了fork创建了子进程。
而这个时候,缓冲区接收的数据没有满,所以这个时候不论哪个进程先退出,都会将数据写入到C语言中的缓冲区当中,最终造成了打印出来的数据有两项是重复的。
而write为什么只打印一次?这是因为write函数是系统调用 ,并 不参与 语言层的缓冲区,所以只打印一次。
当某一个进程退出时,那么一定要将自己缓冲区中的数据刷新到内核当中,而 刷新的本质就是写入 !而一旦写入就会 立马发生 写时拷贝,子进程就有自己的缓冲区,将数据写入到缓冲区中,子进程退出后就会造成二次刷新。
而这个现象也恰恰说明了语言层是存在缓冲区的。
✈️编码模拟
为了更加深刻理解缓冲区这个概念,我们不妨编写一段代码来加深印象:
bash准备:
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:
c
#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:
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:
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结构体内,每一个文件都有自己的缓冲区。
- 缓冲区满了会刷新到内核中,而 刷新的本质就是写入。
希望这篇文章能够帮到你【玫瑰】~~