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

fd与FILE---基础文件IO(关注我,教我写博客 -- 今天带了点颜色)

本节重点
• 复习C⽂件IO相关操作
• 认识⽂件相关系统调⽤接⼝
• 认识⽂件描述符,理解重定向
• 对⽐fd和FILE,理解系统调⽤和库函数的关系
• 理解⽂件和内核⽂件缓冲区
• ⾃定义shell新增重定向功能
• 理解Glibc的IO库

1. 理解"⽂件"

1-1 狭义理解

• ⽂件在磁盘⾥
• 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
• 磁盘是外设(即是输出设备也是输⼊设备)
• 磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO

1-2 ⼴义理解

• Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程)(后⾯会讲如何去理解)

1-3 ⽂件操作的归类认知

• 对于 0KB 的空⽂件是占⽤磁盘空间的
• ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
• 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作

1-4 系统⻆度

• 对⽂件的操作本质是进程对⽂件的操作
• 磁盘的管理者是操作系统
• ⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的

2. 回顾C⽂件接⼝

2-1 hello.c打开⽂件

#include <stdio.h>
int main()
{
	FILE *fp = fopen("myfile", "w");
	if(!fp){
		printf("fopen error!\n");
	}
	while(1);
	fclose(fp);
	return 0;
}

打开的myfile⽂件在哪个路径下?
• 在程序的当前路径下,那系统怎么知道程序的当前路径在哪⾥呢?
可以使⽤ls /proc/[进程id] -l 命令 查看当前正在运⾏进程的信息:

[bbb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249 533463 R+ 1002 7:45 ./myProc
536281 536542 536541 536281 pts/250 536541 R+ 1002 0:00 grep --
color=auto myProc
[bbb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
......
-r--r--r-- 1 hyb hyb 0 Aug 26 17:01 cpuset
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
-r-------- 1 hyb hyb 0 Aug 26 17:01 environ
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
dr-x------ 2 hyb hyb 0 Aug 26 16:54 fd

其中:
• cwd:指向当前进程运⾏⽬录的⼀个符号链接。
exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。
打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS就能知道要创建的⽂件放在哪⾥。

2-2 hello.c写⽂件

include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("myfile", "w");
	if(!fp){
		printf("fopen error!\n");
	}
	const char *msg = "hello bit!\n";
	int count = 5;
	while(count--){
		fwrite(msg, strlen(msg), 1, fp);
	}
	fclose(fp);
	return 0;
}

2-3 hello.c读⽂件

#include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("myfile", "r");
	if(!fp){
		printf("fopen error!\n");
		return 1;
	}
	char buf[1024];
	const char *msg = "hello bit!\n";
	while(1){
	//注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明
		ssize_t s = fread(buf, 1, strlen(msg), fp);
		if(s > 0){
		buf[s] = 0;
		printf("%s", buf);
		}
		if(feof(fp)){
			break;
		}
	}
	fclose(fp);
	return 0;
}

稍作修改,实现简单cat命令:

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("argv error!\n");
		return 1;
	}
	FILE *fp = fopen(argv[1], "r");
	if(!fp){
		printf("fopen error!\n");
		return 2;
	}
	char buf[1024];
	while(1){
		int s = fread(buf, 1, sizeof(buf), fp);
		if(s > 0){
			buf[s] = 0;
			printf("%s", buf);
		}
		if(feof(fp)){
			break;
		}
	}
	fclose(fp);
	return 0;
}

##2-4 输出信息到显⽰器,你有哪些⽅法

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg = "hello fwrite\n";
	fwrite(msg, strlen(msg), 1, stdout);
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	return 0;
}

2-5 stdin & stdout & stderr

• C默认会打开三个输⼊输出流,分别是stdin, stdout, stderr
• 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,⽂件指针

2-6 打开⽂件的⽅式

r Open text file for reading.
	The stream is positioned at the beginning of the file.
r+ Open for reading and writing.
	The stream is positioned at the beginning of the file.
w Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.

w+ Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.

a Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.

a+ Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.

如上,是我们之前学的⽂件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎,忘记了的童鞋回头看看。

3. 系统⽂件I/O

打开⽂件的⽅式不仅仅fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅案。不过,在学习系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接⼝中会使⽤到:

3-1 ⼀种传递标志位的⽅法

#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
	if (flags & ONE) printf("flags has ONE! ");
	if (flags & TWO) printf("flags has TWO! ");
	if (flags & THREE) printf("flags has THREE! ");
	printf("\n");
}

int main() {
	func(ONE);
	func(THREE);
	func(ONE | TWO);
	func(ONE | THREE | TWO);
	return 0;
}

操作⽂件,除了上⼩节的C接⼝(当然,C++也有接⼝,其他语⾔也有),我们还可以采⽤系统接⼝来进⾏⽂件访问, 先来直接以系统代码的形式,实现和上⾯⼀模⼀样的代码:

3-2 hello.c 写⽂件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
	if(fd < 0){
		perror("open");
		return 1;
	}
	int count = 5;
	const char *msg = "hello bit!\n";
	int len = strlen(msg);
	while(count--){
		write(fd, msg, len);//fd: 后⾯讲, msg:缓冲区⾸地址, len: 本次读取,期望写
		⼊多少个字节的数据。 返回值:实际写了多少字节数据
	}
	close(fd);
	return 0;
}

3-3 hello.c读⽂件

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
		perror("open");
		return 1;
	}
	const char *msg = "hello bit!\n";
	char buf[1024];
	while(1){
		ssize_t s = read(fd, buf, strlen(msg));//类⽐write
		if(s > 0){
			printf("%s", buf);
		}
		else{
			break;
		}
	}
	close(fd);
	return 0;
}

3-4 接⼝介绍

open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的⽬标⽂件
flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏“或”运算,构成
flags。
参数:
		O_RDONLY: 只读打开
		O_WRONLY: 只写打开
		O_RDWR : 读,写打开
			这三个常量,必须指定⼀个且只能指定⼀个
O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问
权限
	O_APPEND: 追加写
返回值:
	成功:新打开的⽂件描述符
	失败:-1

mode_t理解:直接 man ⼿册,⽐什么都清楚。
open 函数具体使⽤哪个,和具体应⽤场景相关,如⽬标⽂件不存在,需要open创建,则第三个参数表⽰创建⽂件的默认权限,否则,使⽤两个参数的open。
write read close lseek ,类⽐C⽂件相关接⼝。

3-5 open函数返回值

在认识返回值之前,先来认识⼀下两个概念: 系统调⽤库函数
• 上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数
(libc)。
• ⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
• 回忆⼀下我们讲操作系统概念时,画的⼀张图
在这里插入图片描述
系统调⽤接⼝和库函数的关系,⼀⽬了然。
所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。

3-6 ⽂件描述符fd

• 通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数

3-6-1 0 & 1 & 2

• Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0, 标准输出1, 标准错误2.
• 0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器
所以输⼊输出还可以采⽤如下⽅式

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if(s > 0){
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	}
	return 0;
}

在这里插入图片描述
⽽现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

对于以上原理结论我们可通过内核源码验证:

⾸先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.01160.71.1.el7.x86_64/include/linux/sched.h (3.10.0-1160.71.1.el7.x86_64是内核版本,可使⽤ uname -a ⾃⾏查看服务器配置, 因为这个⽂件夹只有⼀个,所以也不⽤刻意去分辨,内核版本其实也随意)

• 要查看内容可直接⽤vscode在windows下打开内核源代码
• 相关结构体所在位置
◦ struct task_struct : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/sched.h
◦ struct files_struct : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/fdtable.h
◦ struct file : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/fs.h
在这里插入图片描述

3-6-2 ⽂件描述符的分配规则

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

输出发现是 fd: 3
关闭0或者2,在看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

发现是结果是: fd: 0 或者 fd 2 ,可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

3-6-3 重定向

那如果关闭1呢?看代码:

int main()
{
	close(1);
	int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	exit(0);
}

此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常⻅的重定向有: > , >> , <

那重定向的本质是什么呢?
在这里插入图片描述

3-6-4 使⽤ dup2 系统调⽤

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
	int fd = open("./log", O_CREAT | O_RDWR);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	for (;;) {
	char buf[1024] = {0};
	ssize_t read_size = read(0, buf, sizeof(buf) - 1);
	if (read_size < 0) {
		perror("read");
		break;
	}
	printf("%s", buf);
	fflush(stdout);
	}
	return 0;
}

printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1,
但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。那追加和输⼊重定向如何完成呢?

3-6-5 在minishell中添加重定向功能

void ParseRedir(char command_buffer[], int len)
{
	int end = len - 1;
	while(end >= 0)
	{
		if(command_buffer[end] == '<')
		{
			redir = InputRedir;
			command_buffer[end] = 0;
			filename = &command_buffer[end] + 1;
			TrimSpace(filename);
			break;
		}
		else if(command_buffer[end] == '>')
		{
			if(command_buffer[end-1] == '>')
			{
				redir = AppRedir;
				command_buffer[end] = 0;
				command_buffer[end-1] = 0;
				filename = &command_buffer[end]+1;
				TrimSpace(filename);
				break;
			}
			else
			{
				redir = OutputRedir;
				command_buffer[end] = 0;
				filename = &command_buffer[end]+1;
				TrimSpace(filename);
				break;
			}
		}
		else
		{
			end--;
		}
	}
}
void DoRedir()
{
	// 1. 重定向应该让⼦进程⾃⼰做!
	// 2. 程序替换会不会影响重定向?不会
	// 0. 先判断 && 重定向
	if(redir == InputRedir)
	{
		if(filename)
		{
			int fd = open(filename, O_RDONLY);
			if(fd < 0)
			{
				exit(2);
			}
				dup2(fd, 0);
		}
		else
		{
			exit(1);
		}
	}
	else if(redir == OutputRedir)
	{
		if(filename)
		{
			int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
			if(fd < 0)
			{
				exit(4);
			}
			dup2(fd, 1);
		}
		else
		{
			exit(3);
		}
	}
	else if(redir == AppRedir)
	{
		if(filename)
		{
			int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
			if(fd < 0)
			{
				exit(6);
			}
			dup2(fd, 1);
		}
		else
		{
			exit(5);
		}
	}
	else
	{
	// 没有重定向,Do Nothong!
	}
}

4. 理解“⼀切皆⽂件”

⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西,使⽤的接⼝跟⽂件接⼝也是⼀致的。

这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。

之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在/usr/src/kernels/3.10.01160.71.1.el7.x86_64/include/linux/fs.h 下,以下展⽰了该结构部分我们关系的内容:

struct file {
		...
		struct inode *f_inode; /* cached value */
		onst struct file_operations *f_op;
		...
		atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向
		它,就会增加f_count的值。
		unsigned int f_flags; // 表⽰打开⽂件的权限
		fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有
		的标志在头⽂件<fcntl.h> 中定义
		loff_t f_pos; // 表⽰当前读写⽂件的位置
		...
	} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
	```
	值得关注的是 struct file 中的 f_op 指针指向了⼀个file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。
	
	```cpp
	truct file_operations {
	struct module *owner;
	//指向拥有该模块的指针;
	loff_t (*llseek) (struct file *, loff_t, int);
	//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
	EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
	"signed size" 类型, 常常是⽬标平台本地的整数类型).
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返
	回值代表成功写的字节数.
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
	loff_t);
	//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
	loff_t);
	//初始化设备上的⼀个异步写.
	int (*readdir) (struct file *, void *, filldir_t);
	//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返-ENODEV.
	int (*open) (struct inode *, struct file *);
	//打开⼀个⽂件
	int (*flush) (struct file *, fl_owner_t id);
	//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
	int (*release) (struct inode *, struct file *);
	//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
	int (*fsync) (struct file *, struct dentry *, int datasync);
	//⽤⼾调⽤来刷新任何挂着的数据.
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实现.
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
	int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
	long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
	size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
	size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};

file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。

介绍完相关代码,⼀张图总结:
在这里插入图片描述
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。

5. 缓冲区

5-1 什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区

5-2 为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。

⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作

5-3 缓冲类型

标准I/O提供了3种类型的缓冲区。
• 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。

• ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。

• ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。

除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执⾏flush语句;
include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
	close(1);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0) {
		perror("open");
		return 0;
	}
	printf("hello world: %d\n", fd);
	close(fd);
	return 0;
}

我们本来想使⽤重定向思维,让本应该打印在显⽰器上的内容写到“log.txt”⽂件中,但我们发现,程序运⾏结束后,⽂件中并没有被写⼊内容:

[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
 log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$

这是由于我们将1号描述符重定向到磁盘⽂件后,缓冲区的刷新⽅式成为了全缓冲。⽽我们写⼊的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘⽂件中。怎么办呢?可以使⽤fflush强制刷新下缓冲区。

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
	close(1);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0) {
		perror("open");
		return 0;
	}
	printf("hello world: %d\n", fd);
	fflush(stdout);
	close(fd);
	return 0;
}

还有⼀种解决⽅法,刚好可以验证⼀下stderr是不带缓冲区的,代码如下:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
	close(2);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0) {
		perror("open");
	}
	perror("hello world");
	close(fd);
	return 0;
}

这种⽅式便可以将2号⽂件描述符重定向⾄⽂件,由于stderr没有缓冲区,“hello world”不⽤fflash就可以写⼊⽂件:

[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
 hello world: Success

5-4 FILE

•因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通过fd访问的。
• 所以C库当中的FILE结构体内部,必定封装了fd

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg0="hello printf\n";
	const char *msg1="hello fwrite\n";
	const char *msg2="hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

来段代码在研究⼀下:

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg0="hello printf\n";
	const char *msg1="hello fwrite\n";
	const char *msg2="hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

运⾏出结果:

hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,⽽ write 只输出了⼀次(系统调⽤)。为什么呢?肯定和fork有关!

• ⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显⽰器是⾏缓冲。

• printf fwrite 库函数+会⾃带缓冲区(进度条例⼦就可以说明),当发⽣重定向到普通⽂件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。

• ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后

• 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。

• 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。

• write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这⾥所说的缓冲区,都是⽤⼾级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过
不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的“上层”, 是对系统调⽤的“封装”,但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

5-5 简单设计⼀下libc库

my_stdio.h

$ cat my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
	int flag; // 刷新⽅式
	int fileno; // ⽂件描述符
	char outbuffer[SIZE];
	int cap;
	int size;
	// TODO
};
typedef struct IO_FILE mFILE;

mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c

$ cat my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
	int fd = -1;
	if(strcmp(mode, "r") == 0)
	{
	fd = open(filename, O_RDONLY);
	}
	else if(strcmp(mode, "w")== 0)
	{
	fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
	}
	else if(strcmp(mode, "a") == 0)
	{
		fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
	}
	if(fd < 0) return NULL;
	mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
	if(!mf)
	{
		close(fd);
		return NULL;
	}
	mf->fileno = fd;
	mf->flag = FLUSH_LINE;
	mf->size = 0;
	mf->cap = SIZE;
	return mf;
}
void mfflush(mFILE *stream)
{
	if(stream->size > 0)
	{
		// 写到内核⽂件的⽂件缓冲区中!
		write(stream->fileno, stream->outbuffer, stream->size);
		// 刷新到外设
		fsync(stream->fileno);
		stream->size = 0;
	}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
	// 1. 拷⻉
	memcpy(stream->outbuffer+stream->size, ptr, num);
	stream->size += num;
	
	// 2. 检测是否要刷新
	if(stream->flag == FLUSH_LINE && stream->size > 0 && stream-
	>outbuffer[stream->size-1]== '\n')
	{
		mfflush(stream);
	}
	return num;
}
void mfclose(mFILE *stream)
{
	if(stream->size > 0)
	{
		mfflush(stream);
	}
	close(stream->fileno);
}

main.c

$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
	mFILE *fp = mfopen("./log.txt", "a");
	if(fp == NULL)
	{
		return 1;
	}
	int cnt = 10;
	while(cnt)
	{
		printf("write %d\n", cnt);
		char buffer[64];
		snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
		cnt--;
		mfwrite(buffer, strlen(buffer), fp);
		mfflush(fp);
		sleep(1);
	}
	mfclose(fp);
}

http://www.kler.cn/a/510603.html

相关文章:

  • Centos7将/dev/mapper/centos-home磁盘空间转移到/dev/mapper/centos-root
  • 于灵动的变量变幻间:函数与计算逻辑的浪漫交织(下)
  • Flink(十):DataStream API (七) 状态
  • OpenAI函数调用迎来重大升级:引入「最小惊讶原则」等软件工程实践,开发体验更上一层楼!
  • uniapp 微信小程序 editor 富文本编辑器
  • 粒子群优化 (PSO, Particle Swarm Optimization) 算法详解及案例分析
  • webpack 4 升级 webpack 5
  • 在三维坐标系中通过四阶矩阵实现平移和旋转
  • macos 搭建 ragflow 开发环境
  • 【机器学习:三十四、贝叶斯分类器:原理、方法及应用】
  • STL简述
  • 2025.1.15——四、布尔注入
  • MDPI的latex文档书写
  • 【数据结构】—— 顺序表的实现与优化:空间管理与增容策略
  • 使用Python开发PPT文本提取工具
  • Spring的Bean详解=Bean别名+作用范围+使用场景
  • 4.Proto 3 语法详解
  • opencv笔记2
  • htmlcssJavaScript网页开发:年会手机号抽奖案例
  • ANSYS FLUENT学习笔记(八)-实战案例-网格划分
  • 使用 CFX 中的标量传输方程对染料冲洗数值建模:以主动脉弓血流为例
  • python轻量级框架-flask
  • 【AI论文】生成式视频模型是否通过观看视频学习物理原理?
  • 【Linux】Linux入门(2)常见指令
  • Jupter安装
  • vscode的字体图标库-icomoon