网络知识 娱乐 【Linux练习生】基础IO(详细)

【Linux练习生】基础IO(详细)

本节我们讲解基础IO的部分,将围绕以下内容进行梳理讲解:

  • 复习C文件IO相关操作
  • 认识文件相关系统调用接口
  • 认识文件描述符,理解重定向
  • 对比fd和FILE,理解系统调用和库函数的关系
  • 理解文件系统中inode的概念
  • 认识软硬链接,对比区别

1.C语言文件IO

C语言中的文件操作函数如下:
在这里插入图片描述

若想详细了解C语言中文件操作函数的具体使用方法,请跳转博文:C语言文件操作

下面我们对部分文件操作函数进行使用:

  • 对文件进行写入操作示例:
#include 
int main()
{
	FILE* fp = fopen("log.txt", "w");
	if (fp == NULL){
		perror("fopen");
		return 1;
	}
	int count = 10;
	while (count--){
		fputs("hello tomn", fp);
		
	}
	fclose(fp);
	return 0;
}

运行程序后,在当前路径下就会生成对应文件log.txt,文件当中就是我们写入的内容。
在这里插入图片描述

  • 对文件进行读取操作示例:
#include  
  2   
  3 int main()  
  4 {  
  5   FILE* fp = fopen("log.txt", "r");  
  6   if (fp == NULL){  
  7     perror("fopen");  
  8     return 1;  
  9   }  
 10   char buffer[64];//定义缓存区  
 11   while (fgets(buffer, sizeof(buffer), fp)/*按行读取*/ ){  
 12                                        
 13     printf("%s", buffer);                                
 14   }                                                      
 15   if(!feof(fp))//判断文件正常退出                            
 16   {                                                      
 17     printf("fgets quit not normal!");                     
 18   }                                                      
 19   else{                                                   
 20     printf("fgets quit normal!");                        
 21   }                                                      
 22   fclose(fp);                                            
 23   return 0;                                              
 24 }                                                        
 25                 

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上。
在这里插入图片描述
这些是我们学习C语言时接触的文件操作,其实,如果学习文件操作,只停留在语言层面,是很难对文件有一个比较深刻的理解的!

我们知道,任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。

stdin、stdout以及stderr实际上都是FILE*类型的。

在这里插入图片描述
当我们的C程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作。

所以说,stdin、stdout以及stderr与我们打开某一文件时获取到的文件指针是同一个概念,这也诠释了所谓的Linux下一切皆文件的含义。

2.系统文件I/O

2.1引入

其实不止是C语言当中有标准输入流、标准输出流和标准错误流,例如C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。这种特性并不是某种语言所特有的,而是由操作系统所支持的。

我们所谓的文件操作(显示器、键盘、文件(磁盘)),其实最终访问的都是硬件,而通过下面的图我们知道:在这里插入图片描述
用户想要访问硬件,是必须要经过操作系统的,我们知道,操作系统呢是不相信任何人的,访问操作系统,需要通过系统调用接口,因此操作系统有一套系统接口来进行文件的访问。

注意:我们操作文件使用的C语言接口、C++接口或是其他语言的接口,实际上是对系统接口进行了封装,我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。

2.2接口介绍

open

系统接口中使用open函数打开文件,open函数的函数原型如下:

int open(const char *pathname, int flags, mode_t mode);
open的三个参数

open的第一个参数

open函数的第一个参数是pathname,表示要打开或创建的目标文件。

  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)

open的第二个参数

open函数的第二个参数是flags,表示打开文件的方式。

其中常用选项有如下几个:

参数选项含义
O_RDONLY以只读的方式打开文件
O_WRNOLY以只写的方式打开文件
O_APPEND以追加的方式打开文件
O_RDWR以读写的方式打开文件
O_CREAT当目标文件不存在时,创建文件

打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

O_WRONLY | O_CREAT

扩展:

系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:

在这里插入图片描述

例如,O_RDONLY、O_WRONLY、O_RDWR 和 O_CREAT在系统当中的宏定义如下:

#define O_RDONLY         00
#define O_WRONLY         01
#define O_RDWR           02
#define O_CREAT        0100

这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项。

int open(arg1, arg2, arg3){
	if (arg2&O_RDONLY){
		//设置了O_RDONLY选项
	}
	if (arg2&O_WRONLY){
		//设置了O_WRONLY选项
	}
	if (arg2&O_RDWR){
		//设置了O_RDWR选项
	}
	if (arg2&O_CREAT){
		//设置了O_CREAT选项
	}
	//...
}

open的第三个参数

open函数的第三个参数是mode,表示创建文件的默认权限。

例如,将mode设置为0666,则文件创建出来的权限如下:

-rw-rw-rw-

但创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。

umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

-rw-rw-r--

若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

umask(0); //将文件默认掩码设置为0

注意: 当不需要创建文件时,open的第三个参数可以不必设置



小贴士:

在这里插入图片描述
我们可以看到,上面框住的分别是对系统接口进行封装后的C接口和系统接口,实现的功能相同,但是系统接口填写的参数却相对复杂,这就体现了C接口更加友好,有一句话说的很好:不是你的生活充满阳光,而是有人替你负重前行~~


拓展

如果一个文件当前没有被打开,这个文件在哪里?

答:在磁盘

如果我创建一个空文件,该文件要不要占磁盘空间?

答:必须的,因为文件有属性(例如:路径、创建时间、名字等)属性也是数据。

因此磁盘文件=磁盘内容+磁盘属性。所以对文件的操作,就是对文件内容和属性的操作。

open的返回值

open函数的返回值是新打开文件的文件描述符。

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。

#include 
#include 
#include
#include 
#include 
int main()
{
	umask(0);
	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%dn", fd1);
	printf("fd2:%dn", fd2);
	printf("fd3:%dn", fd3);
	printf("fd4:%dn", fd4);
	printf("fd5:%dn", fd5);
	return 0;
}

运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的

在这里插入图片描述
我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。

#include                                                                                        
#include 
#include
#include 
#include 
int main()
{
    int fd = open("test.txt", O_RDONLY);
    printf("%dn", fd);
    return 0;
}

>>-1

运行程序后可以看到,打开文件失败时获取到的文件描述符是 -1

实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。
当使用open函数打开文件成功时数组当中的指针++,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。
而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。

关于文件描述符后面会详细讲

close

系统接口中使用close函数关闭文件,close函数的函数原型如下:

int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

write

系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

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

我们可以使用write函数,将buf位置(buf是定义的数组缓存区)开始向后count字节的数据写入文件描述符为fd的文件当中。

  • 如果数据写入成功,实际写入数据的字节个数被返回。
  • 如果数据写入失败,-1被返回。

对文件进行写入操作示例:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
	if (fd < 0){
		perror("open");
		return 1;
	}
	const char* msg = "hello syscalln";
	for (int i = 0; i < 5; i++){
		write(fd, msg, strlen(msg));
	}
	close(fd);
	return 0;
}

运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容。

read

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

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

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回。
  • 如果数据读取失败,-1被返回。

对文件进行读取操作示例:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd = open("log.txt", O_RDONLY);
    if (fd < 0){
		perror("open");
		return 1;
	}
	char ch;
	while (1){
		ssize_t s = read(fd, &ch, 1);
		if (s <= 0){
			break;
		}
		write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
	}
	close(fd);
	return 0;
}

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上。

3.文件描述符fd

所有的文件操作,表现上都是进程执行对应的函数。要操作文件必须先打开文件,将文件相关的属性信息加载到内存。操作系统存在大量的进程,因此系统中会也存在大量打开的文件,那么,OS要不要把打开的文件在内存中(系统中)管理起来呢??

答:要

那么如何管理呢?

操作系统会为每个已经打开的文件创建各自的struct file结构体(文件的属性信息等),然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。

那么进程和文件之间的对应关系是如何建立的?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。在这里插入图片描述
而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。


当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。

在这里插入图片描述
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。

注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。

我们可以看到打开文件时返回的文件描述符默认从3开始?也就是Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。

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

4.重定向

重定向的原理

输出重定向原理:

输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。

例如,如果我们想让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。

在这里插入图片描述


追加重定向原理:

追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。


输入重定向原理:

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。

例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们运行myproc时,就不是从当前键盘读取数据,而是直接从log.txt读取数据,此时log.txt文件时所分配到的文件描述符就是0。

#include 
#include 
#include 
#include 
#include 
int main()
{
	close(0);
	int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
	if (fd < 0){
		perror("open");
		return 1;
	}
	char str[40];
	while (scanf("%s", str) != EOF){
		printf("%sn", str);
	}
	close(fd);
	return 0;
}

运行结果后,我们发现scanf函数不是从键盘读取,而是将log.txt文件当中的数据都读取出来了。

说明一下:
scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。

标准输出流和标准错误流对应的都是显示器,它们有什么区别?

下面代码中分别向标准输出流和标准错误流写入了两行字符串(msg1和msg2)

 #include
 #include
 #include
  2 
  3 int main()
  4 {
  5   const char *msg1="hello 标准输出n";
  6   write(1,msg1,strlen(msg1));
  7 
  8   const char *msg2="hello 标准错误n";
  9   write(2,msg2,strlen(msg2));
 10 
 11 
 12   return 0;                                                                         
 13 }        

直接运行程序,结果很显然就是在显示器(1,2)上输出两行字符串。在这里插入图片描述

这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。

在这里插入图片描述
实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,因此这也叫输出重定向,而并不会对文件描述符是2的标准错误流进行重定向。

那么我们可以将标准输出和标准错误都重定向到 log.txt 吗?

答案时可以的:
在这里插入图片描述

./redir > log.txt 2>&1

这句命令的意思是:

1.把1的内容指向到log.txt中(./redir > log.txt)
2.把1里面内容拷贝到2里( 2>&1)
3.1.2的内容都指向log.txt

使用 dup2 系统调用

在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向.

我们知道,想要完成重定向,只需进行fd_array数组当中元素的拷贝即可,也就是改变数组中存储的地址。例如,我们将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。

在这里插入图片描述
dup2的函数原型如下:

int dup2(int oldfd, int newfd);

【函数功能】:

dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。

【函数返回值】:

dup2如果调用成功,返回newfd,否则返回-1。

使用dup2时,我们需要注意以下两点:

  • 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
  • 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。

例如,我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
	if (fd < 0){
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	printf("hello printfn");
	fprintf(stdout, "hello fprintfn");
	return 0;
}

添加重定向功能到minishell

我们之前模拟实现了简易版的myshell,现在我们向其中添加一个重定向功能。

在myshell当中添加重定向功能的步骤大致如下:

  • 对于获取到的命令进行判断,若命令当中包含重定向符号>、>>或是<,则该命令需要进行处理。
  • 设置type变量,type为0表示命令当中包含输出重定向,type为1表示命令当中包含追加重定向,type为2表示命令当中包含输入重定向。
  • 重定向符号后面的字段标识为目标文件名,若type值为0,则以写的方式打开目标文件;若type值为1,则以追加的方式打开目标文件;若type值为2,则以读的方式打开目标文件。
  • 若type值为0或者1,则使用dup2接口实现目标文件与标准输出流的重定向;若type值为2,则使用dup2接口实现目标文件与标准输入流的重定向。
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
	int type = 0; //0 >, 1 >>, 2 <
	char cmd[LEN]; //存储命令
	char* myargv[NUM]; //存储命令拆分后的结果
	char hostname[32]; //主机名
	char pwd[128]; //当前目录
	while (1){
		//获取命令提示信息
		struct passwd* pass = getpwuid(getuid());
		gethostname(hostname, sizeof(hostname)-1);
		getcwd(pwd, sizeof