【Linux】基础IO_文件系统IO_“一切皆文件”_缓冲区

news2025/1/15 13:52:23

目录

1. 理解"⽂件"

1-1 狭义理解

1-2 ⼴义理解

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

1-4 系统⻆度

访问文件,需要先打开文件!那么是由谁打开文件???

操作系统要不要把被打开的文件管理起来?

2. 回顾C⽂件接⼝

2.1 fopen("文件名","打开方式"); 

snprintf()

打开的myfile⽂件在哪个路径下?

2-2 hello.c写⽂件

2-3 hello.c读⽂件

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

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

2-5 stdin & stdout & stderr

系统调用接口:

open:

对于系统写入write:

结论:

3.系统读文件:

3.1.open返回值:

那么为什么语言层要进行各自的封装呢?

语言为什么要增加自己的可移植性???

文件描述符是什么???

3-2 ⽂件描述符fd

文件描述符的创建:

重定向:

3.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)

4.理解一切皆文件

这里就体现了多态的思想:

5.缓冲区

5-1 什么是缓冲区

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

5-3 缓冲类型

5-4 FILE

5-5 简单设计⼀下libc库:

my_stdio.h

my_stdio.c

main.c

总结不易~本章节对我有很大的收获,希望对你也是!!!


1. 理解"⽂件"

文件大小为0,文件要不要再磁盘上占据空间呢???
是要的,文件=内容+属性,存取操作围绕着内容+属性展开的!

1-1 狭义理解

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

1-2 ⼴义理解

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

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

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

1-4 系统⻆度

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

访问文件,需要先打开文件!那么是由谁打开文件???

是由进程来打开文件!对文件操作,本质是:进程对文件的操作!

操作系统要不要把被打开的文件管理起来?

要!就是先描述,在组织!!!

2. 回顾C⽂件接⼝

2.1 fopen("文件名","打开方式"); 

虽然这里只传入了一个文件名,但是该调用会将当前路径pwd拼接上当前文件名来进行寻找并打开,在之前的创建文件也是同样如此,获取当前位置的pwd后来创建当前路径的文件!
来认识一下snprintf()这个安全的字符串格式化函数,常用于格式化并将结果写入字符数组中。它可以防止因数组边界溢出导致的安全问题。
int snprintf(char *str, size_t size, const char *format, ...);
  • str:目标缓冲区,用于存储格式化后的字符串。
  • size:缓冲区的大小(包括结尾的空字符 \0)。如果 size 为 0,则 snprintf() 不会向目标缓冲区写入任何字符。
  • format:格式字符串,与 printf 的格式类似,支持 %d%s 等格式说明符。
  • ...:可变参数,提供用于格式化的值。
eg:
#include <stdio.h>

int main()
{
	int cnt = 1;
	const char* msg = "hello,bit: ";
	while (cnt < 10) {
		char buffer[1024];
		snprintf(buffer, sizeof(buffer), "%s%d",msg, cnt++);
		printf("%s\n", buffer); // 输出到屏幕
	}
	return 0;
}

snprintf()

1.每次都是写入这个局部变量buffer内;2.然后计算输入的大小,保证不会发生越界;3.输入的内容“字符串”;4.可变参数列表

输出为:

hello,bit: 1
hello,bit: 2
hello,bit: 3
hello,bit: 4
hello,bit: 5
hello,bit: 6
hello,bit: 7
hello,bit: 8
hello,bit: 9

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr:指向要写入数据的内存地址。
  • size:每个数据单元的大小(以字节为单位)。
  • count:写入的数据单元数量
  • stream:目标文件的指针(FILE * 类型,通常通过 fopen 获得)。
eg:
fwrite(buffer,strlen(buffer),1,fp);  

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  1. ptr:目标缓冲区的指针,用于存储读取到的数据。
  2. size:每个数据单元的大小(以字节为单位)。
  3. count:要读取的数据单元数量。
  4. stream:目标文件指针(FILE * 类型,通常通过 fopen 打开文件获得)。
  • 返回成功读取的 数据单元数量count)。
  • 如果返回值小于请求的 count,可能是因为:
    • 文件结束(EOF)。
    • 发生错误。
  • 可以通过 feof()ferror() 来检查文件状态。

feof()函数可以判断是否读取到文件的末尾

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 命令查看当前正在运⾏进程的信息:
[hyb@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
[hyb@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⼿册关于该函数的说明
		size_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>

//cat myfile    
int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("Usage: %s filename\n", argv[0]);
		return 1;
	}

	FLIE* fp = fopen(argv[1], "r")
		if (fp == NULL)
		{
			perror("fopen");
			return 2;
		}

	while (1)
	{
		char buffer[128];
		memset(buffer, 0, sizeof(buffer));
		int n = fread(buffer, sizeof(buffer) - 1, 1, fp);
		if (n > 0) printf("%s", buffer);
		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返回值类型,⽂件指针
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

系统调用接口:

O_CREAT:创建
O_WRONLY:只写
O_TRUNC:清空

open:

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);
可以看出此时的log.txt的权限是随机的:
必须要在open后跟上相应的八进制权限设置:
open指定第三个参数主要就是为了我们的新建文件的权限
但是这里我们设置的是666,可是创建出来的还是664,这是为什么呢?
就是因为存在系统umask的影响!
可以看到这里并没进行清空文件,而是直接进行覆盖式的写入,所以这里仍然缺少一个系统调用,跟语言层面没有关系: O_TRUNC:清空
所以系统调用就要传入以下几个参数:【路径文件名】+【功能:新建,写入,清空】+【权限】
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);

所以由上面看到,想要追加就不能清空:O_APPEND

所以语言是不可能直接访问底层的存储系统的,只是封装了操作系统底层的接口来进行访问

对于系统写入write:

有以下两种再语言层面进行封装的二进制写入和字符写入

结论:

再系统层面上,并不关心你是二进制写入还是字符写入,最终系统都会转换成二进制来进行识别
对于语言层进行封装的二进制 或 字符写入的接口都是调用的系统write!

3.系统读文件:

那么就不能再打开的时候进行新建,因为当前文件不存在还要新建再打开读是没有意义的!所以读文件不需要新建文件,也不需要清空文件,不需要写入文件,只需要转换为二进制 O_RDONLY,只读方式打开文件
int fd = open("log.txt",O_RDONLY);  

这也就是为什么write存在两参数调用的接口,就是为读来准备的

int n = read(fd,buffer,sizeof(buffer)-1);

3.1.open返回值:

那为什么返回值是从3开始打印呢?
因为返回值0,1,2是叫做标准输入,标准输出,标准错误!
其中FILE是C语言提供的一个结构体typedef struct FILE{;;;;;}; 
再OS界面,只认fd即文件描述符,那么大胆猜测这个FILE里面一定是封装了文件描述符!!!
所以顶层无论怎么封装,底层都只认识文件描述符

那么为什么语言层要进行各自的封装呢?

就是因为每个OS的实现不同,就是我们上面写的代码放在windows下就跑步过去,因为OS不同,底层接口实现的就不一样,如果我们实现一种语言来将各个OS进行封装,我们写一套C语言代码,就可以实现跨平台移植的作用,在各个OS上只需呀裁掉别的平台的所有代码,只保留当前OS的代码,就是当前OS的接口封装即可,这样就凸显出了语言的可移植性!

语言为什么要增加自己的可移植性???

就是为了能够满足各个平台的人,让更多人去使用,占有市场利用率,防止被淘汰

文件描述符是什么???

对于上面打印的这些数字是什么呢?
是数组下标~

3-2 ⽂件描述符fd

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

文件描述符的创建:

FILE是由一个结构体来进行封装的,每一个文件的属性都被封装到一个struct_file内,多个struct_file由一个双链表进行链接,这样先描述,在组织;本质上也就是对一个链表进行增删查改
对于整个链表的管理,还设置了一个文件描述符表 struct file *fd array[] 指针数组,来将整个文件链表的每一个节点存入这个指针数组内,也就有了为什么可以打印出fd的值的下标,这个指针数组再由一个struct files_struct *files指针进行指向
通常我们在申请这个文件描述符的时候,通常就是找到一个最小的,没有被使用过的文件描述符,作为一个被新打开的位置

重定向:

首先进入程序close(1)关掉了stdout标准输出,然后此时的系统就将fd分配给1位置进行指向,然后printf()底层封装的就是原来指向stdout的1,现在又重新指向log.txt位置进行打印,就往文本log.txt内进行打印得到该现象!
重定向dup2(int oldfd,int newfd);
作用就是将当前文件描述符覆盖到newfd这个位置,让newfd也指向oldfd的文件处!dup2()后一般要手动关闭oldfd : close(oldfd)
所以重定向的原理是:打开文件的方式 + dup2()
输入重定向:将打开文件设置为只读方式打开,将当前文件设置到标准输入的位置,dup(fd,0);
fgets(buffer,sizeof(buffer),stdin) fgets()函数用于从指定的流(在这个例子中是 stdin,标准输入流)中读取一行字符,并将其存储到 buffer中。成功了就打印buffer~

设置任意文本的输出重定向: 

⽰例代码
#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.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局的命令⾏参数表
char* gargv[argvnum];
int gargc = 0;
// 全局的变量
int lastcode = 0;
// 我的系统的环境变量
char* genv[envnum];
// 全局的当前shell⼯作路径
char pwd[basesize];
char pwdenv[basesize];
// 全局变量与重定向有关
#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3
int redir = NoneRedir;
char* filename = nullptr;
// " "file.txt
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
string GetUserName()
{
	string name = getenv("USER");
	return name.empty() ? "None" : name;
}
string GetHostName()
{
	string hostname = getenv("HOSTNAME");
	return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
	if (nullptr == getcwd(pwd, sizeof(pwd))) return "None";
	snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
	putenv(pwdenv); // PWD=XXX
	return pwd;
	//string pwd = getenv("PWD");
	//return pwd.empty() ? "None" : pwd;
}
string LastDir()
{
	string curr = GetPwd();
	if (curr == "/" || curr == "None") return curr;
	// /home/whb/XXX
	size_t pos = curr.rfind("/");
	if (pos == std::string::npos) return curr;
	return curr.substr(pos + 1);
}
string MakeCommandLine()
{
	// [whb@bite-alicloud myshell]$
	char command_line[basesize];
	snprintf(command_line, basesize, "[%s@%s %s]# ", \
		GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
	return command_line;
}
void PrintCommandLine() // 1. 命令⾏提⽰符
{
	printf("%s", MakeCommandLine().c_str());
	fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) // 2. 获取⽤⼾命令
{
	// 我们认为:我们要将⽤⼾输⼊的命令⾏,当做⼀个完整的字符串
	// "ls -a -l -n"
	char* result = fgets(command_buffer, size, stdin);
	if (!result)
	{
		return false;
	}
	command_buffer[strlen(command_buffer) - 1] = 0;
	if (strlen(command_buffer) == 0) return false;
	return true;
}
void ResetCommandline()
{
	memset(gargv, 0, sizeof(gargv));
	gargc = 0;
	// 重定向
	redir = NoneRedir;
	filename = nullptr;
}
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 ParseCommand(char command_buffer[])
{
	// "ls -a -l -n"
	const char* sep = " ";
	gargv[gargc++] = strtok(command_buffer, sep);
	// =是刻意写的
	while ((bool)(gargv[gargc++] = strtok(nullptr, sep)));
	gargc--;
}
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
	ResetCommandline();
	ParseRedir(command_buffer, len);
	ParseCommand(command_buffer);
	//printf("command start: %s\n", command_buffer);
	// "ls -a -l -n"
	// "ls -a -l -n" > file.txt
	// "ls -a -l -n" < file.txt
	// "ls -a -l -n" >> file.txt
	//printf("redir: %d\n", redir);
	//printf("filename: %s\n", filename);
	//printf("command end: %s\n", command_buffer);
}
void debug()
{
	printf("argc: %d\n", gargc);
	for (int i = 0; gargv[i]; i++)
	{
		printf("argv[%d]: %s\n", i, gargv[i]);
	}
}
//enum
//{
// FILE_NOT_EXISTS = 1,
// OPEN_FILE_ERROR,
//};
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!
	}
}
// 在shell中
// 有些命令,必须由⼦进程来执⾏
// 有些命令,不能由⼦进程执⾏,要由shell⾃⼰执⾏ --- 内建命令 built command
bool ExecuteCommand() // 4. 执⾏命令
{
	// 让⼦进程进⾏执⾏
	pid_t id = fork();
	if (id < 0) return false;
	if (id == 0)
	{
		//⼦进程
		DoRedir();
		// 1. 执⾏命令
		execvpe(gargv[0], gargv, genv);
		// 2. 退出
		exit(7);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if (rid > 0)
	{
		if (WIFEXITED(status))
		{
			lastcode = WEXITSTATUS(status);
		}
		else
		{
			lastcode = 100;
		}
		return true;
	}
	return false;
}
void AddEnv(const char* item)
{
	int index = 0;
	while (genv[index])
	{
		index++;
	}
	genv[index] = (char*)malloc(strlen(item) + 1);
	strncpy(genv[index], item, strlen(item) + 1);
	genv[++index] = nullptr;
}
// shell⾃⼰执⾏命令,本质是shell调⽤⾃⼰的函数
bool CheckAndExecBuiltCommand()
{
	if (strcmp(gargv[0], "cd") == 0)
	{
		// 内建命令
		if (gargc == 2)
		{
			chdir(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 1;
		}
		return true;
	}
	else if (strcmp(gargv[0], "export") == 0)
	{
		// export也是内建命令
		if (gargc == 2)
		{
			AddEnv(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 2;
		}
		return true;
	}
	else if (strcmp(gargv[0], "env") == 0)
	{
		for (int i = 0; genv[i]; i++)
		{
			printf("%s\n", genv[i]);
		}
		lastcode = 0;
		return true;
	}
	else if (strcmp(gargv[0], "echo") == 0)
	{
		if (gargc == 2)
		{
			// echo $?
			// echo $PATH
// echo hello
			if (gargv[1][0] == '$')
			{
				if (gargv[1][1] == '?')
				{
					printf("%d\n", lastcode);
					lastcode = 0;
				}
			}
			else
			{
				printf("%s\n", gargv[1]);
				lastcode = 0;
			}
		}
		else
		{
			lastcode = 3;
		}
		return true;
	}
	return false;
}
// 作为⼀个shell,获取环境变量应该从系统的配置来
// 我们今天就直接从⽗shell中获取环境变量
void InitEnv()
{
	extern char** environ;
	int index = 0;
	while (environ[index])
	{
		genv[index] = (char*)malloc(strlen(environ[index]) + 1);
		strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
		index++;
	}
	genv[index] = nullptr;
}
int main()
{
	InitEnv();
	char command_buffer[basesize];
	while (true)
	{
		PrintCommandLine(); // 1. 命令⾏提⽰符
		// command_buffer -> output
		if (!GetCommandLine(command_buffer, basesize)) // 2. 获取⽤⼾命令
		{
			continue;
		}
		//printf("%s\n", command_buffer);
		//ls
		//"ls -a -b -c -d"->"ls" "-a" "-b" "-c" "-d"
		//"ls -a -b -c -d">hello.txt
		//"ls -a -b -c -d">>hello.txt
		//"ls -a -b -c -d"<hello.txt
		ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令
		if (CheckAndExecBuiltCommand())
		{
			continue;
		}
		ExecuteCommand(); // 4. 执⾏命令
	}
	return 0;
}

其实文件描述符1标准输出,2标准错误,都是指向同一个文件,说明只做了标准输入的重定向,并没有做出标准错误的重定向:

就想把标准错误给重定向到指定文件呢?
为什么非要存在标准错误呢?printf()、perror???cout/cerr
就是因为他们单独占据文件描述符,可以通过重定向能力,把常规消息和错误消息进行分离!方便日志的形成
那如何把标准输出和标准错误都追加到一个文件内呢??
log.txt就相当于是文件描述符3,然后3进行覆盖到1内,然后文件描述符1就指向该文件,进行标准输出写入,然后重定向到log.txt内
2 > &1就是再将文件描述符1里面的内容覆盖的写入2内,此时的2也跟1一样指向log.txt文件,也可以进行重定向写入

4.理解一切皆文件

⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。
这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。 之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/ fs.h 下,以下展⽰了该结构部分我们关系的内容:
值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。

这里就体现了多态的思想:

  • 是面向对象编程中的一个重要特性,它允许使用统一的接口来表示不同的行为。在 C++ 中,多态主要通过虚函数(Virtual Function)来实现。
  • 简单来说,多态就是 “多种形态”。例如,对于不同类型的动物(如猫、狗),它们都有 “叫” 这个行为,但叫声不同。多态可以让我们通过一个共同的 “叫” 函数接口,来实现不同动物的不同叫声。
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");
		return 0;
	}
	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;
}
运⾏出结果:
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标准库提供。
如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; /usr/include/stdio.h

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.coloradmin.cn/o/2253833.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode】498.对角线遍历

无论何时何地&#xff0c;我都认为对于一道编程题&#xff0c;思考解法的时间用于是实际动手解决问题的2倍&#xff01;如果敲键盘编码需要5min&#xff0c;那么思考解法的过程至少就需要10分钟。 1. 题目 2. 思想 其实这就是一道模拟题&#xff0c;难度中等。做这种题的关键就…

Jupyter Lab打印日志

有时候在 jupyter 中执行运行时间较长的程序&#xff0c;且需要一直信息&#xff0c;但是程序执行到某些时候就不再打印了。 可以开启 日志控制台&#xff0c;将日志信息记录在控制台中。 参考&#xff1a;https://www.autodl.com/docs/jupyterlab/

Hbase整合Mapreduce案例1 hdfs数据上传至hbase中——wordcount

目录 整合结构准备java API 编写pom.xmlMain.javaMap.javaReduce 运行 整合结构 准备 上传hdfs data.txt数据 data.txt I am wunaiieq QAQ 123456 Who I am In todays interconnected world the role of technology cannot be overstated It has revolutionized the way we …

temu登录接口逆向分析(含Py纯算)

文章目录 1. 写在前面2. 接口分析3. 算法还原 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python…

python中什么叫做脚本

Python一种面向对象的动态类型语言&#xff0c;是一种脚本语言&#xff0c;常用于各种服务器的维护和自动化运行。它具有丰富和强大的库。它常被称为胶水语言&#xff0c;因为它能够把用其他语言制作的各种模块很轻松地联结在一起。 以.py 结尾的文件就是Python脚本&#xff0…

全面解析 Transformer:改变深度学习格局的神经网络架构

目录 一、什么是 Transformer&#xff1f; 二、Transformer 的结构解析 1. 编码器&#xff08;Encoder&#xff09; 2. 解码器&#xff08;Decoder&#xff09; 3. Transformer 模型结构图 三、核心技术&#xff1a;注意力机制与多头注意力 1. 注意力机制 2. 多头注意力&…

虚幻引擎---材质篇

一、基础知识 虚幻引擎中的材质&#xff08;Materials&#xff09; 定义了场景中对象的表面属性&#xff0c;包括颜色、金属度、粗糙度、透明度等等&#xff1b;可以在材质编辑器中可视化地创建和编辑材质&#xff1b;虚幻引擎的渲染管线的着色器是用高级着色语言&#xff08;…

基于Matlab计算机视觉的车道线识别与前车检测系统研究

随着自动驾驶技术的发展&#xff0c;车道线识别和前车检测成为智能驾驶系统中的核心技术之一。本实训报告围绕基于计算机视觉的车道线识别与前车检测系统展开&#xff0c;旨在通过处理交通视频数据&#xff0c;实时检测车辆所在车道及其与前车的相对位置&#xff0c;从而为车道…

vue.js学习(day 19)

自定义创建项目 ESlint 代码规范 代码规范错误 手动修正 自动修正 settings.json {"emmet.triggerExpansionOnTab": true,"editor.fontSize": 25,// 当保存的时候&#xff0c;eslint自动帮我们修复错误"editor.codeActionsOnSave": {&qu…

物联网——WatchDog(监听器)

看门狗简介 独立看门狗框图 看门狗原理&#xff1a;定时器溢出&#xff0c;产生系统复位信号&#xff1b;若定时‘喂狗’则不产生系统复位信号 定时中断基本结构&#xff08;对比&#xff09; IWDG键寄存器 独立看门狗超时时间 WWDG(窗口看门狗) WWDG特性 WWDG超时时间 由于…

医疗挂号|基于springBoot的医疗挂号管理设计与实现(附项目源码+论文+数据库)

目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存…

lobeChat安装

一、安装Node.js version > v18.17.0 二、下载 cd F:\AITOOLS\LobeChat git clone https://github.com/lobehub/lobe-chat.git &#xff08;下载要是失败就手动下&#xff1a;https://codeload.github.com/lobehub/lobe-chat/zip/refs/heads/main&#xff09; npm install …

电子应用设计方案-38:智能语音系统方案设计

智能语音系统方案设计 一、引言 智能语音系统作为一种便捷、自然的人机交互方式&#xff0c;正逐渐在各个领域得到广泛应用。本方案旨在设计一个高效、准确、功能丰富的智能语音系统。 二、系统概述 1. 系统目标 - 实现高准确率的语音识别和自然流畅的语音合成。 - 支持多种语…

AWS创建ec2实例并连接成功

aws创建ec2实例并连接 aws创建ec2并连接 1.ec2创建前准备 首先创建一个VPC隔离云资源并且有公有子网 2.创建EC2实例 1.启动新实例或者创建实例 2.创建实例名 3.选择AMI使用linux(HVM) 4.选择实例类型 5.创建密钥对下载到本地并填入密钥对名称 6.选择自己创建的VPC和公有子网…

数字逻辑——二进制

目录 1 信息与编码 1.1 什么是信息&#xff1f; 1.2 什么是编码&#xff1f; 2 数制和码制 2.1 数制 3 一些基本概念 3.1 位&#xff08;bit&#xff09; 3.2 字节&#xff08;byte&#xff09; 3.3 数据量的大小表示符号 4 二进制 4.1 二进制简介 4.2 二进制的…

初识TCP(编写回显服务器)

目录 初识TCP&#xff08;编写回显服务器&#xff09;TCP相关的API服务器代码实现客户端代码实现部分代码解释注意事项效果展示 初识TCP&#xff08;编写回显服务器&#xff09; TCP相关的API ServerSocket &#xff1a; 这是socket类&#xff0c;对应到网卡&#xff0c;但是…

ElasticSearch7.x入门教程之全文搜索聚合分析(十)

文章目录 前言一、指标聚合1、统计最大值&#xff1a;Max Aggregation2、统计最小值&#xff1a;Min Aggregation3、统计平均值&#xff1a;Avg Aggregation4、求和&#xff1a;Sum Aggregation5、Cardinality Aggregation6、基本统计&#xff1a;Stats Aggregation7、高级统计…

【Linux】DNS服务配置

DNS DNS是什么 DNS是Domain Name System的缩写,即域名系统。它是一种用来将域名转化为IP地址的系统。在互联网中,每个网站都有一个唯一的IP地址,但是人们更习惯使用简单易记的域名来访问网站。DNS的作用就是将这些域名转化为对应的IP地址,使得人们可以通过域名来访问网站…

第31天:安全开发-JS应用WebPack打包器第三方库JQuery安装使用安全检测

时间轴&#xff1a; 演示案例&#xff1a; 打包器-WebPack-使用&安全 第三方库-JQuery-使用&安全 打包器-WebPack-使用&安全 参考&#xff1a;https://mp.weixin.qq.com/s/J3bpy-SsCnQ1lBov1L98WA Webpack 是一个模块打包器。在 Webpack 中会将前端的所有资源…

开发者如何使用GCC提升开发效率Opencv操作

看此篇前请先阅读 https://blog.csdn.net/qq_20330595/article/details/144134160?spm=1001.2014.3001.5502 https://blog.csdn.net/qq_20330595/article/details/144134160?spm=1001.2014.3001.5502 https://blog.csdn.net/qq_20330595/article/details/144216351?spm=1001…