Linux 文件系统

目录

介绍

重定向

如何实现重定向

dup2

Linux 一切皆文件深度理解

缓冲区

什么是缓冲区

缓冲区是哪里的

为什么要有缓冲区

缓冲区刷新策略

MyFILE

头文件与宏

MyFILE 结构

InitMyFILE 初始化结构体

myopen 打开文件

myflush 刷新缓冲区

myclose 关闭文件描述符

测试代码


介绍

  • 下面我们说一下本次的学习顺序。
  • 首先我们会理解一下重定向,以及如何是实现重定向。
  • 接着我们理解一下缓冲区,以及缓冲区是在哪里的,还有就是缓冲区的作用是什么。
  • 最后,我们会模仿C语言实现一个自己的 FILE

下面

前面我们已经知道什么是文件描述符了。

  • 文件描述符实际就是数组下标。

  • 也就是 PCB 里面有一个指针数组,里面存的都是 struct file 结构的指针,而该结构体里面保存的是一些关于文件的内容属性,所以只需要找到对应的数组下标就可以找到相关的文件,就可以对对应的文件进行操作。

  • 而文件描述符的分配规则就是找最小的没有被分配的文件描述符。

重定向

如何实现重定向

  • 实际上 Linux 中也是默认打开三个流,stdin、stdout、stderr。

  • 而我们可以理解为,创建一个进程的时候,为进程的 PCB 里面的文件描述符数组的 0、1、2 设置为标准输入、标准输出和标准错误。

  • 所以默认就是打开了三个流,而我们后续打开的进程都是从 3 开始的。

void test2()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  // 打开成功
  printf("fd: %d\n", fd);
​
  const char* str = "hello fprintf\n";
  fprintf(stdout, "%s",str);
  close(fd);
}
  • 这里我们将 str 打印到 stdout 也就是标准输出。

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
fd: 3
hello fprintf

这里确实打印到标准输出了,下面我们要是将 1 号文件描述符关闭,然后再打开一个文件呢:

void test2()
{
  close(1);
​
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  // 打开成功
  printf("fd: %d\n", fd);
​
  const char* str = "hello fprintf\n";
  fprintf(stdout, "%s",str);
}
  • 这里我们将 1 号文件描述符关闭。

  • 然后我们打开 log.txt 文件,接着我们向标准输出中写,会发生什么呢?

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
[lxy@hecs-165234 linux104]$ 
那么我们看一下 log.txt 文件
[lxy@hecs-165234 linux104]$ cat log.txt 
fd: 1
hello fprintf
  • 这里发现 fprintf 向标准输出里写的内容到了 log.txt 文件里面

  • 但是不知道有没有发现,我上面没有关闭打开的 fd 文件(这是一个问题,后面说)。

下面我们将标准输入关闭,然后打开 log.txt 文件,然后从标准输入中读取,然后打印出来,看一下结果:

void test3()
{
  close(0);
  int fd = open("log.txt", O_RDONLY);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  // 打开成功
  printf("fd: %d\n", fd);
  char buffer[64] = {0};
  fread(buffer, sizeof(buffer) - 1, 1, stdin);
  printf("%s\n", buffer);
}
  • 这里关闭 0 号文件描述符(标准输入)

  • 然后打开 log.txt 此时 log.txt 的文件描述符就是 0 号

  • 所以此时向标准输入中读取,就会读取 log.txt 里面

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
fd: 0
fd: 1
hello fprintf

  • 前面,我们关闭了标准输出,打开一个文件,此时该文件的文件描述符就是标准输出,此时向显示器上打印。那么就是向该文件里面打印

  • 还有关闭标准输入,打开一个文件,此时该文件的文件描述符就是标准输入的位置,此时向标准输入中读取就是向该文件中读取。

  • 那么前面的这个效果是什么呢?重定向!

但是我们看到,前面的重定向需要先关闭文件,然后才能打开,那么如果文件已经打开了,那么还可以重定向吗?

可以!

dup2

NAME
       dup, dup2, dup3 - duplicate a file descriptor
​
SYNOPSIS
       #include 
​
       int dup(int oldfd);
       int dup2(int oldfd, int newfd);
  • 该函数的作用就是重定向

  • 这里的 new 是 old 的拷贝,也就是 最后都会变成 old ,所以 old 就是我们想要替换的文件描述符

  • 如果失败的话,那么就返回 -1

下面我们使用该函数试一下:

void test4()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  //打开成功
  printf("fd: %d\n", fd);
  //重定向
  int r = dup2(fd, 1);
  if(r == -1)
  {
    perror("dup2");
    exit(2);
  }
  //向标准输出中写入数据
  const char* str = "hello dup2";
  fprintf(stdout, "%s\n", str);
  close(fd);
}
  • 这里使用了 dup2 函数来重定向。

  • 我们对 log.txt 文件描述符与标准输出的文件描述符进程重定向。

  • 所以我们后面 fprintf 向标准输出中打印,就会打印到 log.txt 文件中。

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
fd: 3
[lxy@hecs-165234 linux104]$ cat log.txt 
hello dup2
  • 这里看到,即使我们最后 close 了 fd 也是可以的(这个和之前不能关闭fd都是同一个问题,这个问题先保留)

下面我们将标准输入与 log.txt 文件进行重定向,看一下是否可以从 log.txt 文件中读取到数据:

void test5()
{
  int fd = open("log.txt", O_RDONLY);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  // 打开成功
  printf("fd: %d\n", fd);
  // 重定向
  dup2(fd, 0);// 标准输入重定向
  char buffer[64] = {0};
  fread(buffer, sizeof(buffer)-1, 1, stdin);
  printf("%s", buffer);
  close(fd);
}
  • 这里将 log.txt 文件与标准输入重定向。

  • 让本应从标准输入中读取到,log.txt 中读取。

  • 然后我们打印读取到的内容。

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
fd: 3
hello dup2

Linux 一切皆文件深度理解

  • Linux 是使用C语言实现的。

  • 那么如果我们想要访问硬件,我们怎么访问呢?

  • 我们访问硬件可以使用 read 和 write 方法访问,也就是 I/O.

  • 但是硬件的结构是不同的,所以访问硬件的 read/write 方法当然也是不同的!

  • 既然方法是不同的,那么为什么我们系统只提供一个 read 和 write 方法呢?

  • 其实可以使用多态!!!

  • 那么C语言如何实现对象呢?如何实现运行时多态呢?

  • 再 Linux 中,想要实现一个对象,但是C语言中只有 struct(结构体)。

  • 我们都知道结构体里面不能放成员函数,那么如何实现呢?

  • 函数指针!

struct file
{
    //成员变量
    int fd;
    ...
    //成员方法
    ssize_t (*read)(int fd, void* buf, size_t count);
    ssize_t (*write)(int fd, const void *buf, size_t count);
}
  • 所以我们可以写一批不同硬件的 read 和 write 方法!

  • 我们再初始化结构体的时候,可以将对应的方法给初始化到对应的函数上。

  • 所以这样就是可以实现对象和运行时多态。

缓冲区

再前面,我们提到了我们再调用printf/fprintf 等函数,有时候刷新不出来,我们现在就来解决这个问题!

什么是缓冲区

  • 其实什么是缓冲区这个问题很好回答,我们自己经常定义的一个 buffer 这样的数组,或者是临时存储数据的,都可以称为缓冲区!

  • 所以缓冲区也就是再内存上的一段数组空间!

缓冲区是哪里的

上面我们知道了什么是缓冲区,那么我们当然也得知道缓冲区是谁的?

那么缓冲区是谁的呢?

我们在看一下那个试验:

void test6()
{
  close(1);
  // 
  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  const char* str1 = "fprintf you can see me!\n";
  const char* str2 = "write you can see me!\n";
  fprintf(stdout, "%s", str1);
  write(1, str2, strlen(str2));
  close(fd) ;
}
  • 这里关闭了标准输出,然后打开了 log.txt 文件。

  • 这里到后面关闭了 log.txt 文件描述符。

结果:

[lxy@hecs-165234 linux104]$ ./myfile 
[lxy@hecs-165234 linux104]$ cat log.txt 
write you can see me!
  • 为什么只有 write 函数被写到 log.txt 文件里面了?

  • 为什么 C 的 IO接口调用的函数没有写到 log.txt 文件。

  • 那么现在就有问题了,如果这里有缓冲区的话,那么这个缓冲区是在哪里的?

  • 显然这个缓冲区一定是在C语言上。

  • 如果是系统上的,那么C语言的也是调用了 write 函数,那么为什么系统函数写到文件了,C语言的函数没有呢?

  • 那么这里怎么样让他写到文件里面呢?

  • 可以在关闭文件的时候强制刷新缓冲区!

void test6()
{
  close(1);
  // 
  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  const char* str1 = "fprintf you can see me!\n";
  const char* str2 = "write you can see me!\n";
  fprintf(stdout, "%s", str1);
  write(1, str2, strlen(str2));
  fflush(stdout);
  close(fd) ;
}

结果:

[lxy@hecs-165234 linux104]$ cat log.txt 
write you can see me!
fprintf you can see me!
  • 这里强制刷新后就有数据了,那么就更能说明是C语言的!

为什么要有缓冲区

  • 那么为什么要有缓冲区呢?

  • 下面举一个例子!

  • 现在有一个卡车司机,他现在有货物需要从北京拉到上海。

  • 那么当他现在只有一件小货物的时候,他就从北京到上海,但是由于北京到上海很远,所以每次去依次都会很麻烦,并不是因为货太多而麻烦,而是因为从北京到上海很麻烦。

  • 所以此时卡车司机就有三种策略:

  • 当是不重要的货物时,那么卡车司机就可以等到这一卡车装满的时候再送货。

  • 当是比较重要的货物时,那么可以装到一半的时候,或者不到一半的时候就要去送。

  • 当时特别重要的货物时,就需要一旦有货物就送。

  • 实际上缓冲区也是这三种刷新策略。

缓冲区刷新策略
  1. 立即刷新

  2. 行刷新

  3. 全缓冲

  • 实际上,硬件的话,他们都是倾向于全缓冲的,也就是缓冲区被打满之后才刷新。

  • 但是又由于不同的硬件需求不同,所以刷新策略是不同的。

  • 因为不同的硬件需求不同,所以不仅要考虑效率,还要考虑体验,例如显示器,就采用行刷新。

  • 而一般的磁盘文件而言,他们一般采用的是全缓冲。

  • 所以这里我们也就知道了,为什么我们向 log.txt 文件里面写数据,然后我们中间关闭文件描述符他就不会写到文件里面,但是如果我们不关闭文件描述符的话,他又会刷新进去,这是如果缓冲区还有数据的话,那么当进程结束的时候也会刷新到文件里面。

  • 但是也可以强制刷新,也就是 fflush。

下面我们继续对缓冲区理解一下:

void test7()
{
  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }
  // 打开成功
  //dup2(fd, 1);
  const char* str1 = "hello write\n";
  const char* str2 = "hello fprinte\n";
  const char* str3 = "hello fputs\n";
  const char* str4 = "hello fwrite\n";
  
  write(1, str1, strlen(str1));
  fprintf(stdout,"%s", str2);
  fputs(str3, stdout);
  fwrite(str4, strlen(str4), 1, stdout);
  
  fork();
}
  • 我们看这一段代码,这里我们是打开 log.txt 文件。

  • 然后使用系统 write 函数打印一条数据。

  • 使用C 的IO函数打印三条数据。

首先我们向显示器打印:

[lxy@hecs-165234 linux104]$ ./myfile 
hello write
hello fprinte
hello fputs
hello fwrite

然后我们重定向到 log.txt 文件里面:

[lxy@hecs-165234 linux104]$ ./myfile > log.txt 
[lxy@hecs-165234 linux104]$ cat log.txt 
hello write
hello fprinte
hello fputs
hello fwrite
hello fprinte
hello fputs
hello fwrite
  • 这里发现,系统函数打印了一条数据。

  • 而C语言函数都打印了两条。

  • 我们把 fork 屏蔽掉。

向显示器打印:

[lxy@hecs-165234 linux104]$ ./myfile 
hello write
hello fprinte
hello fputs
hello fwrite

重定向到 log.txt 文件:

[lxy@hecs-165234 linux104]$ ./myfile > log.txt 
[lxy@hecs-165234 linux104]$ cat log.txt 
hello write
hello fprinte
hello fputs
hello fwrite
  • 这里看到我们关闭掉 fork j就没有刚才的现象了,说明刚才的现象确实与 fork 有关。

解释

  • 前面我们看到了刚才现象确实与 fork 有关。

  • 那么我们这时候就需要想一下, fork 会做什么呢?然后导致有两份数据被刷新到了文件里面。

  • 在 fork 的时候,我们前面的代码已经执行完了(也就是打印数据代码)。

  • 那么显然不可能是因为创建子进程后执行了这些代码。

  • 但是 fork 创建子进程会和父进程代码共享,而数据发生写时拷贝。

  • 那么C语言是有缓冲区的,而且这些数据暂时还没有刷新到文件里面。

  • 那么这些数据算不算是父进程的数据呢?

  • 算父进程的数据!

  • 既然算是父进程的数据,那么创建子进程的时候会不会写时拷贝呢?

  • 会的!

  • 既然会写时拷贝,那么不论哪一个进程在先将缓冲区中的数据刷新的时候,不就是对缓冲区中的数据发生了写入吗?

  • 所以不就会发生写时拷贝吗?

  • 所以此时就会有两份数据,都会被刷新到文件里面。

  • 但是系统调用没有缓冲区(这里说的可不是内核没有缓冲区),所以就不会被写入两份。

MyFILE

下面我们可以写一个自己的基于系统调用的文件:

首先,我们需要一个自己的文件的结构体:

  • 首先我们的结构体里面需要有缓冲区,可以减少IO次数,提高效率。

  • 然后我们还要有 fd ,也就是文件描述符,因为我们一定会调用系统调用,而系统调用值认文件描述符。

  • 由于我们有缓冲区,所以我们需要记录缓冲区的最后一个位置。

  • 而我们缓冲区的刷新策略就是行刷新。

头文件与宏

#include
#include
#include
#include
#include
#include
#include
#include
​
​
#define NUM 128
  • 下面我们使用的函数与宏都是来自上面的头文件和宏的定义。

MyFILE 结构

// 我的文件描述符
typedef struct MyFILE
{
  int _fd;         // 文件描述符
  char buffer[NUM];// 缓冲区
  int end;         // 缓冲区结尾位置
} MyFILE;
  • 这就是我们的文件描述符,里面有缓冲区,文件描述符与end.

  • end 这里所指的是缓冲区里面最后一个字符的下一个位置,所以也表示该缓冲区中有多少数据。

InitMyFILE 初始化结构体

既然我们有 MyFILE 结构,所以我们也需要初始化该结构的函数:

void InitMyFILE(MyFILE* *fd, int filed)
{
  *fd = (MyFILE*)malloc(sizeof(MyFILE));// 为 MyFILE 申请空间
  // 初始化
  (*fd)->_fd = filed;
  memset((*fd)->buffer, 0, NUM);
  (*fd)->end = 0;
}

myopen 打开文件

我们当然也需要打开文件的一个函数:

  • 该函数,我们模仿C语言。

  • 其中的权限都是采用和C语言相同的逻辑。

MyFILE* myopen(const char* path, const char* mode)
{
  // path 和 mode 不能为空
  assert(path);
  assert(mode);
  // 查看 mode 是什么
  MyFILE* fd = NULL;
  if(strcmp(mode, "r") == 0)
  {
    int t = open(path, O_RDONLY);
    if(t >= 0)
      InitMyFILE(&fd, t);
  }
  else if(strcmp(mode, "r+") == 0)
  {
    int t = open(path, O_WRONLY | O_RDONLY);
    if(t >= 0)
      InitMyFILE(&fd, t);
  }
  else if(strcmp(mode, "w") == 0)
  {
    int t = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(t >= 0)
      InitMyFILE(&fd, t);
  }
  else if(strcmp(mode, "w+") == 0)
  {
    int t = open(path, O_RDONLY | O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(t >= 0)
      InitMyFILE(&fd, t);
  }
  else if(strcmp(mode, "a") == 0)
  {
   int t = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666);
   if(t >= 0)
    InitMyFILE(&fd, t);
  }
  else if(strcmp(mode, "a+") == 0)
  {
    int t = open(path, O_RDONLY | O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(t >= 0)
      InitMyFILE(&fd, t);
  }
  else 
  {
    printf("权限出错\n");
  }
  return fd;
}

myflush 刷新缓冲区

该函数时刷新缓冲区的,当我们需要刷新缓冲区的时候我们就调用该函数,或者是当我们调用 close 的时候,我们也需要刷新缓冲区:

void myflush(MyFILE* fd)
{
  assert(fd);
  //将 buffer 中的数据刷新到文件
  write(fd->_fd, fd->buffer, fd->end);
  fd->end = 0;
  // 真正刷新到磁盘
  syncfs(fd->_fd);
}
  • 这里刷新后,就需要将 end 在置为 0,表示缓冲区已经清空了。

  • 当我们调用完系统调用 write 之后,由于 write 没有缓冲区,所以这里会刷新到内核中。

  • 但是内核中是由缓冲区的,所以为了真正刷新到磁盘上,我们还需要调用 syncfs 函数。

myclose 关闭文件描述符

当我们关闭文件描述符的时候,我们就需要将缓冲区中的数据都刷新到磁盘,所以在 close 的时候我们需要调用 myflush.

而且刷新完后,由于我们的 MyFILE 是在堆上 malloc 的,所以我们需要释放这块空间。

void myclose(MyFILE* fd)
{
  assert(fd);
  //关闭之前将缓冲区中的数据刷新进磁盘
  if(fd->end)
  {
    myflush(fd);
  }
  //释放 MyFILE 
  free(fd);
}

测试代码

void test1()
{
  MyFILE* fd = myopen("log.txt", "w");
  if(fd == NULL)
  {
    // 打开失败
    exit(1);
  }
  // 打开成功
  const char* str1 = "hello MyFILE\n";
  const char* str2 = "hello MyFILE hello str2 hello fpust";
  const char* str3 = "hello MyFILE hello world hello world hello world";
  const char* str4 = "hello MyFILE hello myclose hello myflush hello world hello nihao shijian\n";
  const char* str5 = "hello MyFILE";
  const char* str6 = "hello MyFILE\n";
  myputs(str1, fd);
  myputs(str2, fd);
  myputs(str3, fd);
  myputs(str4, fd);
  myputs(str5, fd);
  myputs(str6, fd);
​
  myflush(fd);
  myclose(fd);
}

结果:

[lxy@hecs-165234 linux105]$ cat log.txt 
hello MyFILE
hello MyFILE hello str2 hello fpusthello MyFILE hello world hello world hello worldhello MyFILE hello myclose hello myflush hello world hello nihao shijian
hello MyFILEhello MyFILE

上面由于是一个简单的模仿一下,所以并没有其他的IO函数,不过其他的IO接口也都是类似的,而且write接口跟号理解缓冲区,所以上面就以write接口示范。

有兴趣的话,可以自己实现一下其他接口。

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