理解文件

文件大小为0的文件,同样需要在磁盘上占用空间。这是因为文件 = 内容 + 属性,所有对文件的存取和操作,本质上都是围绕内容与属性这两部分展开的。

文件是文件属性(元数据)与文件内容的集合(文件 = 属性(元数据)+ 内容)。因此,所有文件操作都可以归结为对文件内容的操作和对文件属性的操作

在进行文件操作之前,有一个必要的预备步骤:访问文件前,必须先打开文件。那么,是谁来打开文件呢?答案是进程打开文件!因此,对文件的操作本质上是进程对文件的操作

磁盘的管理者是操作系统。文件的读写操作,其底层本质并非通过 C语言/C++ 的库函数直接完成,而是通过操作系统提供的文件相关系统调用接口来实现的

操作系统是否需要管理被打开的文件?答案是肯定的,管理的方式是“先描述,再组织”。

文件分类:
1.内存级文件
2.磁盘级文件

stdin & stdout & stderr

在正式学习基础 I/O 之前,我们先回顾三个基本接口:

C 语言默认会打开三个标准输入输出流,分别是 stdinstdoutstderr

  • 它们的类型都是 FILE*(文件指针),与 fopen 函数的返回值类型相同。
  • stdin:标准输入,通常对应键盘文件。
  • stdout:标准输出,通常对应显示器文件。
  • stderr:标准错误,也通常对应显示器文件。

程序的核心任务就是处理数据

另外,关于文件打开时的行为:

  • 默认情况下,打开文件时文件内容会先被清空
  • 如果以追加模式("a")打开文件,则会在文件末尾追加内容,而不会清空原有内容。
    (在 Shell 中使用 >> 重定向时也是追加模式)

系统文件IO

下面我们先从代码的角度来看一下四组基本的接口:open read write close

int main()
{
	int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);// 以创建/只写方式打开文件 test.txt,权限设置为 0666,返回文件描述符 
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	
	const char *msg = "1234567890";
	int cnt = 5;
	int a = 1234567;
	while(cnt)
	{
		write(fd, msg, strlen(msg));
		
		char buffer[16];
		snprintf(buffer, sizeof(buffer), "%d", a);
		write(fd, buffer, strlen(buffer));
		
		cnt--;
	}
	
	while(true)
	{
		char buffer[64];
		int n = read(fd, buffer, sizeof(buffer) - 1);
		if(n > 0)// 读成功了
		{
			buffer[n] = 0;
			printf("%s", buffer);
		}
		else if(n == 0) break;
	}
	
	close(fd);
	return 0;
}

O_CREAT:文件不存在则创建,存在则直接打开
O_WRONLY:以只写模式打开文件
O_RDONLY:以只读模式打开文件
O_TRUNC:打开并清空文件
O_APPEND:在文件后面追加内容(与O_TRUNC是冲突的)
0666:文件创建后的默认访问权限(实际受系统 umask 影响)

在系统层面不存在文本写入和二进制写入这样的概念。换言之,系统不关心你的写入方式,我们可以随便写。
所谓文本写入和二进制写入时语言层的概念。

Linux 程序默认自带三条固定通道:

  1. 0:标准输入(读数据)
  2. 1:标准输出(打印正常信息)
  3. 2:标准错误(专门打印报错信息)

在这里插入图片描述
FILE是C语言提供的一个结构体typedef XXX{}FILE;open & fopen等相关函数的返回值为FILE,即文件描述符(fd)。在操作系统的接口层面,只认文件描述符

fd文件描述符是什么?
本质就是 Linux 进程中存放文件的数组下标,内核靠这个下标,就能找到进程打开的对应文件。
在这里插入图片描述
读函数本质上使内核到用户空间的拷贝函数!
对文件内容做任何操作都必须先把文件加载(磁盘-内存的拷贝)到内核对应的文件缓冲区内。

重定向

文件描述符的分配原理:找到最小的、没有被使用的文件描述符,作为新的fd给用户。
而重定向的思想就是更改文件描述符的指针指向,数组下标保持不变

重定向的原理:操作系统在原代码中做操作系统级别的文件指针所对应的地址对应的拷贝。
在这里插入图片描述
重定向:打开文件的方式 + dup2
dup2就是重定向函数
在这里插入图片描述

在shell中添加重定向功能

在上一节中我们实现了一个自定义的shell,这里我们在其中再添加一个重定向功能。

// 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

int redir = NONE_REDIR;
std::string filename;

void TrimSpace(char cmd[], int &end)
{
	while(isspace(cmd[end]))
	{
		end++;
	}
}
// 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
void RedirCheck(char cmd[])
{
	redir = NONE_REDIR;
	filename.clear();
	
	int start = 0;
	int end = strlen(cmd) - 1;
	// "ls -a -l > file.txt" > >> <
	while(end > start)
	{
		if(cmd[end] == '<')
		{
			cmd[end++] == 0;
			TrimSpace(cmd, end);
			redir = INPUR_REDIR;
			filename = cmd+end;
			break;
		}
		else if(cmd[end] == '>')
		{
			if(cmd[end - 1] == '>')
			{
				// ">>"
				cmd[end - 1] = 0;
				redir = APPEND_REDIR;
			}
			else
			{
				// '>'
				redir = OUTPUT_REDIR;
			}
			cmd[end++] = 0;
			TrimSpace(cmd, end);
			filename = cmd + end;
			break;
		}
		else end--;
	}
}

为什么程序中存在标准错误
设立标准错误,就是为了把正常输出和错误信息分开,借助重定向就能分别保存日志,方便排查问题、规范日志管理。

如果把stderrstdout打印到同一个文件中会怎么样呢
所有正常打印、报错提示全都挤在同一个标准输出里,信息就混在一起了,根本分不清哪句是正常运行日志、哪句是程序报错。
专门分出标准错误,最大作用就是:把「正常业务信息」和「程序错误信息」从源头彻底分开

“一切皆文件”

我们在前面很早之前就提到过Linux下一切皆文件。那么我们如何理解这个一切皆文件呢?
这里我们引入一个概念:VFS虚拟文件系统

VFS 给所有硬件、普通文件、管道、网络套接字,统一套上同一套抽象外壳,把它们全部伪装成「文件」。
不管你底层是硬盘、键盘还是屏幕,在系统内核眼里:通通都是一个文件

在系统中访问任何设备,只要提供文件描述符,就可以不用关心底层硬件的差异,直接使用内部的函数指针对设备进行管理。
我们通过下图的方式将对设备的管理转化为对链表的增删查改:

缓冲区

什么是缓冲区

缓冲区是内存空间的一部分,在内存空间中预留了一部分存储空间用来缓冲输入或输出,这部分预留的空间就叫做缓冲区。

用户级缓冲区:在应用程序自己的用户空间,是程序层面的缓存。作用是减少频繁发起系统调用,攒够一批数据再交给内核。
文件内核缓冲区:在操作系统内核空间,是内核专门给文件 IO 用的缓存,夹在应用和物理磁盘中间。

打个比方:当我们上网网购,东西到了,快递员将快递送到了。但是我们没有立刻去拿,我们的快递就被放在了菜鸟驿站。我们的快递可以先放在驿站。等到我们什么时候有空了再去驿站取出。那么这个菜鸟驿站就是我们的缓冲区,我们可以将自己的资源先放在缓冲区中,等到什么时候我们有空了,再从缓冲区中将其取出并执行。

缓冲区刷新策略

Linux 标准 IO 有着完善的缓冲区刷新机制,不同缓冲模式适配不同应用场景,从根源优化 IO 性能,同时内核缓冲区还承担着衔接应用与硬件的核心作用。
全缓冲是三种缓冲模式里效率最高的一种,也是我们读写普通磁盘文件时的默认采用方式。它会先把数据暂存到用户缓冲区,等缓冲区存满、程序主动刷新或者进程正常退出时,才一次性批量把数据提交给内核。这种批量处理的方式,极大减少了系统调用和磁盘交互次数,把 IO 开销降到最低,所以文件读写场景都会优先使用全缓冲。
行缓冲主要适配显示器这类终端交互设备,以换行符 \n 作为刷新触发条件。只要输出内容遇到换行符,缓冲区就会立刻刷新、把内容打印到屏幕;若没有换行,数据会暂时缓存不输出,刚好满足人机交互实时查看输出的需求。
文件内核缓冲区拥有独立的刷新策略,完全由操作系统内核自行判断和调度刷新时机。对应用程序而言,只要把数据成功交到操作系统的内核缓冲区,逻辑上就等同于数据已经交付给硬件,无需开发者关心后续何时落盘、怎么落盘。内核会根据系统负载、磁盘空闲状态、内存调度规则,异步择机将内核缓冲区的数据同步到物理磁盘,全程自动管控底层硬件交互。

应用样例

int main()
 {
	 // 库函数
	 printf("hello printf\n");
	 fprintf(stdout, "hello printf\n");
	 const char *s = "hello fwrite\n";
	 fwrite(s, strlen(s), 1, stdout);
	 
	 // 系统调用
	 const char *ss = "hello write\n";
	 write(1, ss, strlen(ss));
	 
	 // ???
	 fork();// 当我们fork执行时,库函数的内容还在缓冲区里面,等退出时,父子各自退出,各自刷新,刷新两次
	 // 像显示器刷新是行刷新,重定向不只是重定向,本质上还更改了文件的刷新方式
	 
	 return 0;
 }

为什么要引入缓冲区

提高效率:提高使用者的效率!谁用就提高谁的效率

模拟一下封装简单的glibc->文件接口

#pragma once
// 文件名叫 "mystdio.h"
#include <stdio.h>
#define MAX 1024
#define NONE_FLUSH (1<<0)// 无缓冲
#define LINE_FLUSH (1<<1)// 行缓冲
#define FULL_FLUSH (1<<2)// 全缓冲

typedef struct IO_FILE
{
	int fileno;
	int flag;
	char outbuffer[MAX];
	int bufferlen;
	int flush_method;
}MyFile;

MyFile *MyFopen(const char *path, const char *mode);
void MyFclose(MyFile *);
int MyFwrite(MyFile *, void *str, int len);
void MyFFlush(MyFile *);
static MyFile *BuyFile(int fd, int flag)
{
	MyFile *f = (MyFile*)malloc(sizeof(MyFile));
	if(f == NULL)return NULL;
	f->bufferlen = 0;
	f->fileno = fd;
	f->flag = flag;
	f->flush_mothod = LINE_FLUSH;
	memset(f->outbuffer, 0, sizeof(f->outbuffer));
	return f;
}

MyFile *MyFopen(const char *path, const char *mode)
{
	int fd = -1;
	int flag == 0;
	if(strcmp(mode, "w") == 0)
	{
		int flag = O_CREAT | O_WONLT | O_TRUNC;
		fd = open(path, flag, 0666);
	}
	else if(strcmp(mode, "a") == 0)
	{
		int flag = O_CREAT | O_WONLT | O_APPEND;
		fd = open(path, flag, 0666);
	}
	else if(strcmp(mode, "r") == 0)
	{
		int flag = O_RDWR;
		fd = open(path, flag);
	}
	else
	{
		// TODO
	}
	
	if(fd < 0)return NULL;
	
	return BuyFile(fd, flag);
}
void MyFclose(MyFile *)
{
	if(file->fileno < 0)return;
	MyFFlush(file);
	close(file->fileno);
	free(file);
}
int MyFwrite(MyFile *file, void *str, int len)
{
	// 写入就是拷贝
	memcpy(file->outbuffer+file->bufferlen, str, len);
	file->bufferlen += len;
	
	// 尝试判断是否满足刷新条件
	if((file->flush_method & LINE_FLUSH) && (file->outbuffer[file->bufferlen-1] == '\n'))
	{
		MyFFlush(file);
	}
	
	return 0;
}
void MyFFlush(MyFile *file)
{
	if(file->buffrtlen <= 0)return;
	int n = write(file->fileno, file->outbuffer, file->bufferlen);
	(void)n;
	file->bufferlen = 0;
}
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐