基础IO——系统IO | 文件描述符fd | 重定向

🌈感谢阅读East-sunrise学习分享——基础IO——系统IO | 文件描述符fd | 重定向
博主水平有限,如有差错,欢迎斧正🙏感谢有你
码字不易,若有收获,期待你的点赞关注💙我们一起进步


🌈文件操作对于程序员来说必不可少
C语言有C语言的文件操作接口,JAVA有JAVA的…
所有我们来一波釜底抽薪🚀从根源入手,学习系统IO等知识

一、重谈文件

  1. 空文件在磁盘中也要占据空间,因为文件 = 内容 + 属性
  2. 文件操作即是对文件的内容、文件的属性进行操作
  3. 标定一个问题,必须使用:文件路径 + 文件名【文件路径+文件名具有唯一性】,如果没有指明对应的文件路径,默认是在当前路径进行文件访问
  4. 文件操作的本质是:进程 和 被打开文件 的关系

二、回顾C语言文件操作

1.文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。若打开成功,返回文件指针;若打开失败,返回NULL

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

2.读写文件

🚀写文件

文件输出有许多函数,就不一一赘述,常用的是fprintf

fprintf是将特定的数据格式化到特定的文件流中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56wRAr3I-1675492112928)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230114203356013.png)]

🚀读文件

fgets从特定文件流中按行读取到缓冲区中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3bLOvrq-1675492112929)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230114204533853.png)]

3.关于stdin stdout stderr

文件的输入输出,实际上便是在与硬件进行交互;而有三个标准流,会随着计算机系统的开启而默认打开:

  1. stdout —— 标准输出流 —— 显示器
  2. stdin —— 标准输入流 —— 键盘
  3. stderr —— 标准错误流 —— 显示器

IO的意思是:输入(input)和输出(output),具体来说是外部设备和内存之间的输入输出

💭而上面的三种流属于IO中的外部设备,而在Linux操作系统中,一切皆文件

所以这三个标准流的类型都是 FILE*

三、系统IO

💭 系统IO是什么?为何要学?

我们知道,文件存在与磁盘中,而磁盘是硬件,那要访问硬件需要通过谁?——操作系统
所以我们进行文件操作不能绕开OS,因此OS也提供了文件级别的系统调用接口
🎯

而我们平时使用的C语言、C++、JAVA…这些上层语言的文件操作接口,这些语言级别的文件操作接口都不相同,但是不论如何,库函数的底层都是调用系统调用接口;也就是说,这些各式语言级别的文件操作接口,其实都是基于系统调用接口去封装而成的。

因此为了降低学习成本,更深刻地了解IO,我们便从最底层的系统IO入手

1.open & close

📚我们可以通过手册查找具体用法:man 2 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

💭文件描述符是什么?我们下文再细说

📚man 2 close

#include <unistd.h>

int close(int fd);

🎯1.open接口的第二个参数原理介绍
我们在写程序的过程中,很经常会用到标记位flags。一般用一个整数(0或1)作为标记位,表示某一件事发生了或作为返回值返回。
一个标记位代表一件事,传一个整数;那10个呢?搞10个参数?太麻烦了吧💤
而我们知道,一个整数是有32个比特位,意味着我们可以通过比特位来传递选项,不同比特位我们自己定义不同的含义
因为我们是按照比特位传递选项,所以势必有要求:1.一个比特位表示一个选项 2.比特位位置不能重复

#include <stdio.h>

//每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
#define ONE 0x1 //也可写成(1<<0)——... 0000 0001
#define TWO 0x2 //也可写成(1<<1) ——... 0000 0010
#define THREE 0x4 //也可写成(1<<2) —... 0000 0100
#define FOUR 0x8//也可写成(1<<3) —... 0000 1000

void show(int flags)
{
   if(flags & ONE) printf("one\n");
   if(flags & TWO) printf("two\n");
   if(flags & THREE) printf("three\n");
   if(flags & FOUR) printf("four\n");
}

int main()
{
   show(ONE);
   printf("----------\n");
   show(TWO);
   printf("----------\n");
   show(ONE | TWO);
   printf("----------\n");
   show(ONE | TWO | THREE);
   printf("----------\n");
   show(ONE | TWO | THREE |FOUR);
   printf("----------\n");

   return 0;
}                                       

在这里插入图片描述

open接口的第二个参数使用原理同理🌰

🎯2.open接口的第三个参数mode

当文件不存在要新建文件时,就需要使用3个参数的接口,使用mode向系统指明要创建文件的权限

否则新建的文件权限说明是乱码

当文件已经存在需要访问文件,便可使用2个参数的函数接口

2.read & write

📚man 2 write

#include <unistd.h>

ssize_t write(int fd, const void* buf, size_t count);

参数:
	buf:想写入的缓冲区
	count:期望写的字节数
返回值:实际写的字节数(ssize_t是Linux系统定义的有符号整型)

🎯在使用write写入时就需要注意使用两个flags选项

O_TRUNC:打开文件的时候直接清空文件
O_APPEND:追加文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14sIUkCK-1675492112930)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230115110859395.png)]

值得注意的是:

  1. 写入文件的过程中,不需要写入\0!因为\0是C语言层面上规定的字符串结束标志,可系统IO并不关心这些;系统IO关心的是写入文件的内容,即有效字符即可
  2. write接口函数的第二个参数是void*,也就是系统IO不会关心你写入的是什么类型的,所以我们要写入什么类型就自己转换成什么再写入

📚man 2 read

#include <unistd.h>

ssize_t read(int fd, void* buf,size_t count);

参数:
	buf:读到的内容放入的用户层缓冲区
	count:期望读的字节数
返回值:实际读的字节数

​	[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lunQeFjY-1675492112930)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118111712743.png)]

值得注意的是:

  1. 读文件的前提是:文件已经存在,所以读文件时不需要涉及创建及权限的问题,因此调用两个参数的open打开文件即可
  2. 从函数的原型可发现第二个参数也是void*,说明read函数也没有类型的概念,需要我们自己去准备;比如上面的代码中,我们认为读到的是字符串,所以要提前创建好并且在尾部手动添加\0(0)

3.文件描述符–fd

💭在上面的练习中,open函数会有一个返回值fd,称为文件描述符🚩文件描述符是什么?有什么用?我们来一探究竟

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqHMQKm4-1675492112931)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118114411326.png)]

我们打开多个文件后打印这些文件的返回值(文件描述符)发现有一些特征

  • fd从3开始,0,1,2不知所在
  • 是连续的小整数 --> 就像数组下标一样

事实上,当我们的系统运行起来时,系统会默认打开三个标准输入输出,因此012其实分别对应的就是标准输入、标准输出、标准错误🚩


💭那文件描述符为什么是连续小整数?它的本质又是什么呢?

💭在上文已经有提及到,文件操作的本质:进程和被打开文件的关系

而进程可以打开多个文件,并且系统中也会有许多进程,如此一来,系统中一定会存在大量的被打开的文件📌而这些大量的被打开的文件需不需要管理呢?-- 肯定需要📌那管理者是谁呢? – 操作系统(OS)📌而提到操作系统进行管理,肯定就会想到我们之前反复介绍的操作系统的管理理念——先描述,再组织📌所以操作系统为了管理对应的文件,必定要为文件创建对应的内核数据结构 ——> struct file { },其中包含了文件的大部分属性

⭕而文件描述符作为一种对文件的标识,也是文件的属性之一,所以文件描述符fd会在struct file { }中
话又说回来,012对应的标准输入输出的类型是FILE*,之前并没有对此类型进行了解,而现在学习至此

  • FILE其实是一个struct结构体
  • 是C语言在struct file结构体的基础上再封装的,内部有多种成员
  • 而作为上层语言的结构,底层一定还是要调用 系统调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hCCQovVV-1675492112931)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118125234800.png)]

🌏知道了操作系统为了管理,会给每个文件创建一个struct file之后再组织管理起来

但是打开的这么多文件,进程又如何知道哪个文件是它的呢?所以为了让进程和文件能够构建联系,操作系统创建了一个结构体struct files_struct来构建文件和进程之间关系,这个结构中又包含了一个数组struct file* fd_array[ ],也就是一个指针数组,进程每新打开一个文件,文件的地址便会填到此指针数组中;而每个进程的PCB里面都也保存了一个指针,这个指针指向了那个属于此进程的数据结构对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znqAaK9Y-1675492112931)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118131748133.png)]

🌏所以现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了struct file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张files_struct(文件描述符表),该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

这也解释了为什么write和read这样的系统调用接口一定要传入文件描述符fd,执行系统调用接口是进程执行的,通过进程PCB,找到自己打开的文件描述符表,再通过fd索引数组找到对应的文件,从而对文件进行操作✔️

✅结论:文件描述符fd本质上就是:进程与被打开文件之间维持关系的数组的下标

1.文件描述符的分配规则

✏️直接看代码

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

发现结果是:fd:0或者fd:2

🚩可见,文件描述符的分配规则:在fifiles_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

4.重定向

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

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

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log.txt当中,其中,fd=1。这种现象叫做输出重定向。

常见的重定向有,> : 输出重定向 >> :追加重定向 < :输入重定向

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZ5aDNjg-1675492112932)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118140111512.png)]

📌所以重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址

1.dup2系统调用

为了支持重定向,系统也有支持重定向的接口,其作用是对文件描述符下标里面的内容进行拷贝

📚函数原型:

#include <unistd.h>
int dup2(int oldfd, int newfd);

dup2的文档解释:

makes newfd be the copy of oldfd, closing newfd first if necessary

翻译来说就是:newfd是oldfd的一份拷贝

🔅所以假设,输出重定向,其操作是要让文件描述符1中的地址不再指向显示器文件,而是指向log.txt;也就是要将log.txt的地址拷贝到1中

因此在调用dup2时即是:dup2(fd,1);

♨️输出重定向

int main()
{
	int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
	assert(fd > 0);
	dup2(fd, 1);
	printf("open fd: %d\n", fd); //printf --> 默认stdout
	fprintf(stdout, "open fd: %d\n", fd); // =printf
	
	fflush(stdout);
	close(fd);
	return 0;
}

注意,系统层面,open打开文件时带了选项O_TRUNC,使得每次打开文件都会清空原来的内容。而在C语言中打开文件的“w”选项,也会使得把原始文件清空,说明上层封装了这个选项

♨️追加重定向

只需在输出重定向的基础上,在打开文件时把O_TRUNC选项改为O_APPEND选项即可

♨️输入重定向

int main()
{
	int fd = open("log.txt", O_RDONLY);
    //输入重定向的前提时文件需存在
	assert(fd > 0);
	dup2(fd, 0);
	
	char line[64];
	while(1)
	{
		if(fgets(line, sizeof(line), stdin) == NULL) beak;
		printf("%s",line);
	}
	close(fd);
	return 0;
}

2.模拟实现重定向

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NONE_REDIR   0 //没有重定向
#define INPUT_REDIR  1 //输入重定向
#define OUTPUT_REDIR 2 //输出重定向
#define APPEND_REDIR  3 //追加重定向

#define trimSpace(start) do{\
		while(isspace(*start)) ++start;\
	}while(0)

int redirType = NONE_REDIR;
char* redirFile = NULL;

//"ls -a -l -i > log.txt" ---> "ls -a -l -i" "log.txt"
void commandCheck(char* commands)
{
	assert(commands);
	char* start = commands;
	char* end = commands + strlen(commands);
	//遍历寻找重定向符号
	while (start < end)
	{
		if (*start == '>')
		{
			*start = '\0';
			++start;
			redirType = OUTPUT_REDIR;
			if (*start == '>')
			{
				*start = '\0';
				++start;
				redirType = APPEND_REDIR;
			}
			//消除空格
			trimSpace(start);
			redirFile = start;
			break;
		}
		else if (*start == '<')
		{
			*start = '\0';
			++start;
			redirType = INPUT_REDIR;
			trimSpace(start);
			redirFile = start;
			break;
		}
		else
		{
			++start;
		}
	}
}

int main()
{
	while (1)
	{
		//每次都要重置一次
		redirType = NONE_REDIR;
		redirFile = NULL;

		//...
		//上篇博客myshell的内容
		//...

		commandCheck(lineCommand);

		//...
		pid_t id = fork();
		assert(id != -1);

		if (id == 0)
		{
			switch (redirType)
			{
			case NONE_REDIR:
				//什么都不做
				break;
			case INPUT_REDIR:
			{
				int fd = open(redirFile, O_RDONLY);
				if (fd < 0)
				{
					perror("open");
					exit(-1);
				}
				//重定向的文件已经成功打开
				dup2(fd, 0);
			}
			break;
			case OUTPUT_REDIR:
			case APPEND_REDIR:
			{
				int flags = O_WRONLY | O_CREAT;
				if (redirType == APPEND_REDIR) 
					flags |= O_APPEND;
				else
					flags |= O_TRUNC;
				int fd = open(redirFile, flags, 0666);
				if (fd < 0)
				{
					perror("open");
					exit(-1);
				}
				dup2(fd, 1);
			}
			break;
			default:
				printf("bug?\n");
				break;
			}
		}
	}
}

四、Linux下一切皆文件

🌏一切皆文件时Linux的设计哲学,体现在操作系统的软件设计层面

在计算机中,像键盘、显示器、磁盘、网卡等硬件称为外设,外设的任何数据要进行处理都得先读到内存中,然后再刷新到其他外设中,这便是IO的过程🔅

我们知道,操作系统为了管理软硬件资源,所以对每个硬件都进行了描述,所以每个设备都有其对应的内核结构体对其描述。而不同的硬件之间都有其不同的读写方法,如果无法读/写,方法可以为空,但是每个硬件都统一拥有IO读写方法,存在于各种硬件匹配的驱动程序之中🔅

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kRTqFZud-1675492112932)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118202637101.png)]

💭每种硬件的访问方法一定是不一样的,那又是如何做到一切皆文件呢?

🌏事物之间,各不相同却又大有相同💭为了便于使用和管理,此时面向对象的思想便诞生了…

但是Linux是C语言写的,如何用C语言实现面向对象,甚至多态呢?

  • 我们知道:类是由成员函数 + 成员方法组成的,而在C语言中,struct也能实现

虽然每个硬件的具体属性不同(比如此硬件的存储情况、数据的读取状态进度…),但是我们遵循面向对象的思想,将这些硬件的各种属性抽象出来,统一起来✅就好比,鸡鸭鹅肯定是不同种生物吧,但是我们可以将他们统一看成“动物类”,而这些鸡鸭鹅便是动物类实例化出来的各种对象

所以Linux在设计时,还设计了struct file结构体,里面包含了每个底层硬件的属性,在这个结构体之中定义了许多属性,便能将各种硬件都笼统起来🔅

struct file
{
	int size;
	mode_t mode;
	int user;
	int group;
	......
	//函数指针
	int (*readp)(int fd, void* buffer, int len);
	int (*writep)(int fd, void* buffer, int len);
	......
}

为了实现一切皆文件,Linux做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file;每个设备都有其对应的结构体对象,里面包含了各种定义、函数指针…而这一切都是在操作系统里面维护的🔅

在struct file上层压根就不关心你底层的每个硬件之间具体的不同的读写方法,他只看到了操作系统维护的struct file,所有文件都是调用统一的接口🔅

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ev9AH6X7-1675492112932)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230118210042946.png)]


🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KGoDY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值