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
🌎Linux基础IO 文章目录: Linux基础IO C语言中IO交互 常用C接口 fopen fputs fwrite fgets 当前路径 三个文件流 系统文件IO open函数 …...

202006青少年软件编程(Python)等级考试试卷(二级)
第 1 题 【单选题】 以下程序的运行结果是?( ) l ["兰溪","金华","武义","永康","磐安","东阳","义乌","浦江"]for s in l:if"义"in s:print(…...

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

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

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

什么是字节码?
字节码(Bytecode)是Java虚拟机(JVM)能够理解和执行的中间代码。Java源代码首先编译成字节码文件(扩展名为 .class),而不是直接编译成特定机器的机器码。字节码具有以下特点: 平台无…...

C++ JWT的使用
接入sdk需要使用JWT加密参数,做个记录以备后查 #include <iostream> #include <jwt-cpp/jwt.h> int main() { // 设置JWT的密钥(对于HS256) 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用得着,miniconda 默认自带的 python 是 3.11 版本,比较新; 安装virsual studio 2019 要把C桌面相关的都安装了,大概需要20G,不要安装到 C 盘,都安装到…...

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

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

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

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

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

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

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

Java为什么会成为现在主流的编程语言
Java为什么会成为现在的主流语言 前言一、Java语言概述Java是什么为什么大多数人会选择从事Java为什么从事Java的工作者数量从年递减 二、Java语言的特点简单性面向对象分布式(微服务)健壮性安全性体系结构中立可移植性解释型高性能多线程动态性 三、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等来定位标签
目录: 1、使用背景2、代码处理 1、使用背景 客户使用我们产品组件,发现替换文件,每次替换都会新增如下的样式,造就样式错乱,是组件的文件,目前临时处理的话就是替换文件时删除新增的样式,但是发…...

机器人工具箱学习(三)
一、动力学方程 机器人的动力学公式描述如下: 式中, τ \boldsymbol{\tau} τ表示关节驱动力矩矢量; q , q ˙ , q \boldsymbol{q} ,\; \dot{\boldsymbol { q }} ,\; \ddot{\boldsymbol { q }} q,q˙,q分别为广义的关节位置、速度和加速…...

华为OD机试 - CPU算力分配(Java 2024 C卷 100分)
华为OD机试 2024C卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试(JAVA)真题(A卷B卷C卷)》。 刷的越多,抽中的概率越大,每一题都有详细的答题思路、详细的代码注释、样例测试…...

web前端框架设计第八课-表单控件绑定
web前端框架设计第八课-表单控件绑定 一.预习笔记 1.v-model实现表单数据双向绑定 2.搜索数据的实现 3.全选案例实现1—JQ方法 4.单选案例实现 5.数据级联(二级级联) 6.v-model中的修饰符 二.课堂笔记 三.课后回顾 –行动是治愈恐惧的良药,…...

这三个网站我愿称之为制作答辩PPT的神
很多快要毕业的同学在做答辩PPT的时候总是感觉毫无思路,一窍不通。但这并不是你们的错,对于平时没接触过相关方面,第一次搞答辩PPT的人来说,这是很正常的一件事。一个好的答辩PPT可以根据以下分为以下几部分来写。 1.研究的背景和…...

flutter开发实战-实现多渠道打包及友盟统计(亲测有效)
flutter开发实战-实现多渠道打包及友盟统计(亲测有效) 最近开发过程中,需要引入友盟进行统计服务。友盟统计还需要区分不同渠道的打开应用的情况,所以需要处理多渠道打包的问题。 一、引入友盟统计 在工程的pubspec.yaml中引入…...

JavaScript-JSON对象
JSON格式 JSON(JavaScript Object Notation, JS对象简谱)是一种轻量级的数据交换格式。它基于ECMAScript(European Computer Manufacturers Association, 欧洲计算机协会的一个子集,采用完全独立于编程语言的文本格式来存储和表示…...

【C语言】自定义类型之---结构体超详解(结构体的定义使用、指针结构体,内存对齐,......代码详解)
目录 前言: 一:结构体 1.1:什么是结构体? 1.2:结构体类型的声明 1.3:结构体变量的定义 1.4:结构体的内存对齐 1.5:结构体传参 二:位段 2.1:位段是什…...

【完美恢复】修复计算机中丢失emp.dll的多个详细方法
最近,在尝试运行某款游戏时,我遭遇了一个令人头痛的问题——“emp.dll文件丢失”。这个错误通常意味着游戏的某个关键文件没有被正确加载或已损坏。以下是我解决问题的步骤和一些心得体会,希望对遇到类似问题的玩家们有所帮助。 emp.dll是一…...

暗黑4可以搬砖吗?暗黑4怎么搬砖 搬砖攻略
暗黑4可以搬砖吗?暗黑4怎么搬砖 搬砖攻略 暗黑破坏神4属于是暴雪旗下一款经典游戏IP,在全世界有着广泛的玩家群体,更是在今年暴雪国服宣布回归之后,吸引了一大批新玩家加入。今天小编就为大家带来暗黑4的详细搬砖教程。 现在我们…...

WLAN技术
冲突域:连接在同一传输线缆上的所有工作站的集合,或者说是同一物理网段上所有节点的集合共同竞争网络资源形成的域叫冲突域。 在OSI模型中,冲突域被看作是第一层的概念,连接同一冲突域的设备有中继器、集线器(hub&…...

维修AB罗克韦尔工控机 PanelView 900 2711-T9C8 SER C 触摸屏人机界面
可视化和 HMI 解决方案可帮助您满足生产力、创新和全球化需求。为电子操作员界面终端、分布式客户端/服务器 HMI 和信息软件提供了一致的外观和感觉。编程工具和高级软件应用程序包括远程访问和数据分析,可加速开发并提高效率。 图形终端 图形终端提供各种尺寸、操…...