【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode

目录

  • 回顾一下C语言的文件操作
  • 系统文件I/O
    • 系统IO写文件
    • 系统IO读文件
  • open函数返回值
  • 文件描述符
  • 文件描述符的分配规则
  • 系统调用接口dup2
  • 缓冲区
  • inode

秃头侠们好呀,今天来说 系统IO

回顾一下C语言的文件操作

FILE*fp=fopen("./log.txt","w");//以写的形式打开
FILE*fp=fopen("./log.txt","r");//以读的形式打开
FILE*fp=fopen("./log.txt","a");//以追加的形式打开
int main()
{
	FILE* fp = fopen("myfile", "w");
	if (!fp) {
		printf("fopen error!\n");
	}
	const char* msg = "hello world!\n";
	int count = 5;
	while (count--) {
		fwrite(msg, strlen(msg), 1, fp);
	}
	fclose(fp);
	return 0;
}
int main()
{
	FILE* fp = fopen("myfile", "r");
	if (!fp) {
		printf("fopen error!\n");
	}
	char buf[1024];
	const char* msg = "hello world!\n";
	while (1) {
		ssize_t s = fread(buf, 1, strlen(msg), fp);
		if (s > 0) {
			buf[s] = 0;
			printf("%s", buf);
		}
	}
	fclose(fp);
	return 0;
}
fputs("hello world\n", fp);//写入,没有就创建log.txt再写入
char buffer[64];
fgets(buffer, sizeof(buffer), fp);//读取到buffer

这里就不一一举例说明C语言的文件操作了。
想看可以看这篇具体C语言文件操作

C语言默认打开 stdin stdout stderr三个流
C++默认打开 cin cout cerr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

以上所有写入的操作都要往文件或者硬件设备写入,而文件也就是磁盘,也是硬件。最终都是访问硬件,而OS我们之前说了,它是硬件的管理者,所以所有语言上对文件的操作都要贯穿OS。

而我们也知道,OS不相信任何人,所以访问OS需要通过系统调用接口。

几乎所有语言,封装的比如fopen fclose fread fwrite等等,底层一定要使用OS提供的系统调用接口。
下面我们就说说系统文件IO

系统文件I/O

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第1张图片

int main()
{
  int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
  int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);
  int fd2=open("./log.txt",O_WRONLY|O_CREAT,0644);
  int fd3=open("./log.txt",O_WRONLY|O_CREAT,0644);
  int fd4=open("./log.txt",O_WRONLY|O_CREAT,0644);
  if(fd<0)
  {
    perror("open");
  }
  printf("fd:%d\n",fd);
  printf("fd:%d\n",fd1);
  printf("fd:%d\n",fd2);
  printf("fd:%d\n",fd3);
  printf("fd:%d\n",fd4);

  close(fd);
  close(fd1);
  close(fd2);
  close(fd3);
  close(fd4);
  return 0;
}

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第2张图片

如果对fd进行打印,发现fd为3,如果再不断打开其他文件,发现fd为4,5,6,7。。。为什么会有这样的现象,0,1,2去哪了?

答:fd为文件描述符后面具体说,0,1,2分别对应着
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第3张图片
-1为打开失败。
我们现在根据fd,012345…能联想到什么?数组下标!后面慢慢解释。

所有的文件操作,表现上都是进程执行对应的函数,进程对文件的操作为,先打开文件,然后将文件相关的属性信息加载到内存,系统中是不是存在大量的进程,进程可以打开多个文件,那是不是系统中会有更多打开的文件,那么OS要不要管理起来?很明显需要!那如何管理?先描述,再组织
struct file{…}

系统IO写文件

int main()
{
  int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
  
  if(fd<0)
  {
    perror("open");
    return 1;
  }

  const char*msg="hello world!\n";
  int cnt=5;
  while(cnt--)
  {
    write(fd,msg,strlen(msg));//返回值为实际写入的字节数
  }//这就在log.txt写入了5条hello world!

  close(fd);
  return 0;
}

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第4张图片

系统IO读文件

int main()
{
  int fd=open("./log.txt",O_RDONLY);
  if(fd<0)
  {
    perror("open");
    return 1;
  }
  char buffer[1024];
  ssize_t s=read(fd,buffer,sizeof(buffer)-1);
  if(s>0)
  {
    buffer[s]=0;
    printf("%s\n",buffer);
  }
  close(fd);
return 0;
}

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第5张图片

open函数返回值

open的返回值是OS给我们的。进程打开文件,OS内一定打开了更多的文件,OS必须对这些文件进行管理。

一个文件没有打开的时候它在哪?在磁盘上!
创建一个空文件,该文件也要属性,属性就是数据,那么该文件就会占磁盘空间。
磁盘文件=文件内容+文件属性。
所以可以对文件内容操作,也可以对文件属性操作(权限、命名、位置等)。

文件描述符

我们之前说了,系统中有很多打开的文件,那么OS就要对其进行管理,怎么管理?一定是先描述,再组织!
先描述再组织就是说要有结构体和组织起来结构体的数据结构

描述文件的结构体是:
struct file{…//文件相关属性};
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第6张图片

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

所以我们还可以用以下方式去输出到显示器上
int main()
{
  const char*msg="hello world\n";
  write(1,msg,strlen(msg));
  write(2,msg,strlen(msg));

  char buff[64];
  ssize_t s=read(0,buff,sizeof(buff)-1);
  buff[s]=0;
  printf("echo# %s\n",buff);
  return 0;
}

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第7张图片

文件描述符的分配规则

给新文件分配的fd,是从fd_arr[]指针数组中找一个最小的,没有被使用的,作为新的fd。

 close(0);
 int fd=open("./log.txt",O_WRONLY|O_CREAT);
 printf("hello world\n");

比如我们如果先close(0),那么再int fd=open(“log.txt”…)
现在的fd就是0,close(2),那么就用2。

但是如果关闭close(1)呢?有个奇怪的现象,没有打印任何东西,而是输出到文件中了,我们竟然完成了一个重定向!
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第8张图片
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第9张图片
为啥嘞?

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第10张图片【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第11张图片
printf为C语言层的打印函数,它的本质是向标准输出打印。
C语言默认会打开:

FILE*stdin
FILE*stdout
FILE*stderr

而FILE*是C语言层上的结构体指针,指向一个结构体

struct FILE{
//一定包含了一个整数是对应在系统层面的
//是这个文件打开对应的fd
};

在系统层面现在的fd==1的数组下标已经指向了log.txt,但是C语言层面结构体内的fd仍未1,它是不能管OS怎么做的,C语言层它只认识fd=1。
所以我们printf在语言层我们还是向stdout(fd=1)打印,然后要调用系统接口,让OS做事,给OS说我要向fd=1打印输出语句了,你帮我弄一下。此时OS系统层因为fd=1指向的是log.txt,就把语句输出到此文件中了。这就完成了一次输出重定向工作。

int fd=open("./log.txt",O_WRONLY|O_CREAT|O_APPEND);
//这样是追加重定向。

输入重定向:

//本来要从标准输入进行读取,现在重定向,从log.txt文件读取到line
int main()
{
  close(0);
  int fd=open("./log.txt",O_RDONLY);
  printf("fd:%d\n",fd);
  char line[128];
  while(fgets(line,sizeof(line)-1,stdin))
  {
    printf("%s\n",line);
  }
return 0
}

我们验证一下,C语言默认打开的三个流结构体内存在fd

  printf("stdin->%d\n",stdin->_fileno);
  printf("stdout->%d\n",stdout->_fileno);
  printf("stderr->%d\n",stderr->_fileno);
  FILE*fp=fopen("./log.txt","r");
  if(fp==NULL)
  {
    perror("fopen");
    return 1;
  }
  printf("fp->%d\n",fp->_fileno);

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第12张图片

系统调用接口dup2

我们非得先关闭0,1,2才能完成重定向吗?

下面有个系统调用接口:

#include 
int dup2(int oldfd, int newfd);

如果执行dup2(fd,1),作用如下图
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第13张图片
重定向前,若newfd已经有打开的文件,就先关闭它,重定向后,oldfd和newfd都指向oldfd的文件。

下面我们来使用dup2来实现一下输出输入重定向

//输出重定向
int fd=open("./log.txt",O_WRONLY|O_TRUNC);
  dup2(fd,1);
  printf("hello world1\n");
  fprintf(stdout,"hello world2\n");
  fputs("hello world3\n",stdout);

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第14张图片

//输入重定向
int fd=open("./log.txt",O_RDONLY);
  dup2(fd,0);
  char buff[1024];
  scanf("%s",buff);
  printf("%s\n",buff);

在这里插入图片描述

执行exec*程序替换的时候,会不会影响我们曾经打开的所有文件呢?
答:不会!
替换只是替换代码和数据,没有创建新的进程,不影响进程,更不会影响到PCB中的管理打开文件的数据结构。

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第15张图片
父进程如果曾经打开了标准输入,标准输出,标准错误,那么子进程也会继承下去。
为什么我们所有进程都会默认打开这仨?
因为bash操纵系统打开的,而之后的都是bash的子进程所有都继承了。

以上我们所有代码最后都没有close(fd),当我们写了话,会发现,重定向到log.txt的内容没有了,怎么回事?这就引出来缓冲区的概念了。我们往后继续看!

简述一下对文件描述符的理解?

在进程中每打开一个文件,都会创建相应的文件描述信息struct file,这个描述信息被添加在PCB的struct file_struct中,以数组的形式进行管理,该数组是指针数组,数组每个元素指向一个文件描述体struct file,文件描述符就是该数组下标,让用户通过文件描述符找到对应的文件,操作文件。

简述一下重定向的实现原理?

每个文件描述符都是内核PCB中文件描述信息数组的下标,每个下标里的数组元素对应一个文件的描述体struct file,用于操作文件。而重定向就是在不改变所操作文件描述符的情况下,通过改变文件描述符下标对应数组元素中文件描述信息,让其指向其他的文件,进而实现改变操作的文件(比如dup2系统调用接口)。

缓冲区

  const char*msg1="hello 标准输出\n";
  write(1,msg1,strlen(msg1));
  const char*msg2="hello 标准错误\n";
  write(2,msg2,strlen(msg2));
  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");
  close(1);

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第16张图片
现在一切正常,但如果我是./test > log.txt呢?
我们会发现:
①显示器只输出hello 标准错误
②log.txt里面只有hello 标准输出

这些原因到底是怎么回事?

首先解释一点,> 重定向是将fd=1的标准输出重定向,所以标准错误的输出不受影响,因为标准错误的文件描述符为2

const char*msg2="hello 标准错误\n";
  write(2,msg2,strlen(msg2));

我们接着解释:
首先我们得了解一下用户到OS的刷新策略:
①立即刷新(不缓冲)。
②行刷新(行缓冲\n)比如向显示器打印。
③全缓冲,缓冲区满了才刷新,比如向磁盘文件写入。

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第17张图片
所以现在解释一下为啥这两条没有重定向到log.txt

printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");

因为:当我们close(1),没有重定向时,这两条语句最后都有\n所以是行刷新策略,遇到\n就直接从C缓冲区刷新到OS内核缓冲区了,最后也就可以被输出。
但是当我们重定向,行刷新的策略变为全缓冲,需要将C缓冲区写满才会刷新到OS缓冲区,这时close(1),数据还来不及刷新到OS内核缓冲区,因为fd关闭了,无法通过fd,将数据再刷新到OS缓冲区了,所以最后也无法将数据写入磁盘文件。log.txt就没有了。

那为什么这句可以写入?

  const char*msg1="hello 标准输出\n";
  write(1,msg1,strlen(msg1));

因为人家可是直接write的系统调用函数呀,人家可是在内核层的,是立即刷新的。

如果你想解决可以在close()前加上fflush。

再来看一个现象

int main()
{
  const char*msg1="hello 标准输出\n";
  write(1,msg1,strlen(msg1));

  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");
  fputs("hello fputs\n",stdout);

  fork();
return 0;
}

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第18张图片
正常运行打印正常,但如果重定向到log.txt为什么,系统调用函数没有拷贝,而C语言接口函数都拷贝了一份?
原因是:
重定向后刷新策略改变,从行刷新变成全缓冲,这时数据会暂时保存在C语言缓冲区,当你fork()创建子进程,因为C语言缓冲区里有数据,要将数据写入到OS缓冲区,会发生写时拷贝。所以结果就知道了。
如果不想发生这种情况,需要在fork()前添加fflush(stdout),这样,C语言缓冲区没有数据,就不会发生写时拷贝了。

不仅仅是C语言,任何上层语言,都存在缓冲区。

inode

之前我们谈到的都是打开的文件,那如果一个文件没有被打开,它在哪里保存着呢?
在磁盘上!

文件=文件内容+文件属性

磁盘是我们计算机中的一个机械设备。

磁盘写入基本单位是:扇区为512字节。
盘面,磁道,磁头,扇区。。。


我们是不是可以把盘片想象成线性结构。
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第19张图片
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第20张图片

【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第21张图片
具体是什么样的呢?
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第22张图片
【Linux操作系统】系统文件IO详解、文件描述符、重定向、缓冲区、inode_第23张图片
我们还看到Struct inode结构体里还有一个int inode_number,这个就是inode编号!
文件名在系统层面是没有意义的,文件名是给用户用的,在Linux中,真正标识一个文件是通过文件的inode编号!一个文件对应一个inode对应其编号!

这就好像国家统计我们百姓信息,不是靠姓名标识哪个人(万一有重名),而是靠每个人的身份证号。

ls -i file 可以查看文件file的inode号

说完inode Table 和 Data block,还有Block Bitmap和inode Bitmap是干嘛的?

有没有想过,我们是怎么快速知道,哪个inode和block是否已经被使用了呢?
这两个位图就起作用了!

拿inode Bitmap举例

0000 1010

从右向左
比特位的位置含义:inode编号
比特位的内容含义:特定inode编号是否被占用

目录也是文件,所以也有inode编号,也有数据块,那放什么呢数据块,比如放文件名和inode编号的映射关系等。
我们所有创建的文件都在一个特定目录下。

如果我们想删除一个文件怎么办?
只需要将位图里相应比特位1置为0即可!

我们有时听说谁谁谁删库跑路了,那如果不小心误删了文件怎么办?
那就是啥也被做,让懂哥过来恢复把0->1。不让你乱动就是怕你把刚才删除那块空间覆盖了,就不好恢复了。


⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流

你可能感兴趣的:(Linux,linux,运维,服务器,c++)