文件系统、描述符和缓冲区

文章详细介绍了文件系统的操作,包括open接口的使用,文件描述符fd的概念及其分配规则,以及write和read接口的差异。此外,还讨论了文件重定向和缓冲区的作用,强调了缓冲区在提高IO效率中的重要性。
摘要由CSDN通过智能技术生成

目录

🏆一、文件系统

 1、open

①对open接口的介绍

②接口使用

 2、write接口

 3、read接口

🏆二、深入理解文件描述符fd

1、fd具体实质

 2、文件fd的分配规则

 3、fd重定向

 ①输出重定向

 ②追加重定向

③输入重定向

④文件的引用计数

🏆三、缓冲区的理解

🏆一、文件系统

1、空文件,也要在磁盘占据空间。

2、文件=内容+属性。

3、文件操作=对内容  + 对属性 or  对内容和属性

4、标定一个文件,必须使用:文件路径+文件名 [唯一性]

5、如果没有指明对应的文件路径,默认是在当前路径下进行文件访问

对文件的操作本质是进程对文件的操作!

一个文件要被访问,就必须先被打开!

文件操作是十分重要的,不同的语言都有自己独特的文件操作接口。C、C++、Java、python、php、shell等语言都有自己独特的文件操作接口。如果我们要全部掌握这些接口成本是很高的!而如果我们重新考虑文件的位置,以及我们访问文件所必经的路径:

所以为了降低学习成本,就要掌握系统调用接口

 批量化注释的方式:

 1、open

①对open接口的介绍

C语言中打开文件的函数是fopen

fopen底层是调用了系统的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: 追加写
 O_TRUNC:清空文件再写入
返回值:
 成功:新打开的文件描述符
 失败:-1

open接口的使用方式和fopen不太一样。首先在C语言中fopen成功返回FILE*指针,那是C中的概念,在操作系统中open接口调用成功则返回file descriptor(文件描述符),它是比C中指针更底层的东西。这一点文件描述符到底还有什么玄妙之处后续再详细介绍。

首先我们来关注open接口的第二个参数:flagsflags是标定打开文件的方式。它是用来作标记的,比如我们在C中常用bool值做标记,但是它只能标记true or false。如果文件显然打开方式有很多,只这两种是不满足我们的需求的。比如以只读、只写、读写、追加等方式打开。而我们要实现这一功能,需要对flags进行独特的设计。Linux借用int的特性,因为int32个bit位OS使用每一个bit位来做标记,那么它到底是怎么实现的呢?我们可以借助演示一段代码:

这里的宏ONE TWO THREE FOUR就是类似于一个个的选项。并且这里采用一个比特位表示一个选项,彼此位置不重叠。

我们在使用这些选项采用的是| 操作。

明白了这些,我们就可以简单使用一下open操作。

②接口使用

以只写方式打开一个不存在的文件。

 我们发现以只写形式,系统不会自动创建文件,那是因为OS不会自己创建。C语言是经过了封装,如果我们想让它以只写形式的同时还要创建,要再 | 上创建选项。

然而这里的创建的log.txt是没法使用的,因为它的权限是随机的,我们在创建文件的时候要加上文件的权限。这一点也是open接口第三个参数的意义:权限

 

所以我们也能理解了为什么OS准备了两套open接口 :

对于已经存在的文件打开时是不需要设置权限的

同时,如果我们想修改创建文件的权限,可以修改umask值。

因为文件权限设置的方式是:

umask默认权限是0002.

 

 当然这里flags选项并没有讲完,它需要结合其他接口来演示效果更佳。

 2、write接口

 在了解了open是如何使用之后,我们再来看一下write的使用方式。老规矩,先来段演示代码:

write接口fwrite是不一样的。我们在使用fwrite接口的时候,因为strlen不计入'\0',所以我们要+1,为'\0'预留一个位置。而我们在使用write接口的时候,是否需要也预留一个位置给'\0'呢?

那我们不妨打印输出一下:

 

显然+1结果并不如我们所愿。那么出现这种情况是因为,以'\0'作为字符串的结尾,是C语言的规定,和文件没有关系。而系统看来,它的文件并不需要'\0'它只要字符串的有效内容,所以使用系统接口write向文件写入字符串并不需要给'\0'留位置!! 

我们再来看一段代码:

 

我们发现,我们以只写打开文件操作系统并没有为我们清空文件。这一点也是和C语言不同的。

C语言对这部分做了处理,会自动清空文件重写,而默认操作系统的文件写入是覆盖,它没有清空里面的内容

如果我们想重写文件时先清空再写入,需要添加选项:O_TRUNC

 

至此,O_WRONLY选项是只写,如果文件不存在不会自动创建,而O_CREAT选项则是文件不存在就创建,默认情况下,对文件写入是覆盖,不会清空文件,所以O_TRUNC是清空文件选项

那么如果我想实现追加呢? O_APPEND

 

 3、read接口

ssize_t 是一种系统定制的类型,是有符合整数,可以等于0,大于0,小于0.

从特定的文件fd当中,将数据读到缓冲区里,期望读多少个,就是count

演示:

 

 

 至此,我们可以看一下语言对文件的操作本质就是对系统调用接口的封装

🏆二、深入理解文件描述符fd

1、fd具体实质

 我们再来理解一下文件。通过对系统调用接口open、write、read的认识,我们大概清楚文件操作的本质就是进程和被打开文件的关系。而系统中存在大量的文件,其中有的没被打开存储在磁盘上,而被打开的文件就要被OS管理起来。到底是如何管理的呢?

操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构标识文件:

struct file{} ,而这其中包含了文件的大部分属性。

结构体file里面有文件的属性,也有文件描述符。这样讲起来还是有点抽象,我们可以画个图更深入理解:

 当文件被打开的时候,它的地址被计入到文件指针数组中,进程通过指针数组管理。而文件描述符本质上讲就是这个指针数组的下标。也就是标识打开文件在指针数组中的位置。而键盘、鼠标、显示器这些也是文件!

当然以上所述是结论,还是举些例子:

 

我们看到新打开文件的fd文件描述符从3开始,既然他们表示数组下标,为什么不是从0开始呢?这是因为默认打开了三个标准输入输出流

 

 

那么他们的返回值不是文件描述符,而是结构体指针,说明结构体中包含了文件描述符。

验证:

 

果然他们的fd是0、1、2.所以我们打开的文件都是从3开始。

补充:FILE结构体

在/usr/include/libio.h路径下:


struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

 2、文件fd的分配规则

文件fd的分配规则是从指针数组下标,从小到大,按照顺序寻找最小的且没有被占用的下标fd

举例说明:

 

 再运行文件时,没有办法看到printf的内容。这是因为,按照有空插空的原则,这时fd数组下标为1的地方被文件myfile占用,而OS角度来看,它只会内容输出到fd为1(标准输出)的文件。这时就会出现无法打印到屏幕的现象。

 3、fd重定向

本来应该输出到显示器,但是这里输出打印到log.txt,这称为重定向

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

这里的上层用的fd不变指的是在上层看来标准输入输出的文件描述符一直是0和1.

简单来说,OS不会去具体查看struct file* 的内容而是根据下标来执行

 ①输出重定向

针对重定向,OS为我们提供了一个接口专门用于重定向。

oldfd下标指向的内容拷贝到newfd下标指向。

 

 ②追加重定向

③输入重定向

 

 这里我们要解决几个问题,子进程重定向会影响父进程吗?

我们知道,进程具有独立性,所以子进程不能影响父进程,所以说file*表父子进程是不一致的!

 所以对于父子进程来说,子进程要拷贝父进程的进程管理,而如果对子进程进行重定向,那么就会file*表发生改变,而这一操作是不影响父进程的(进程具有独立性)。而文件是属于系统部分的,不需要拷贝给子进程。他们只是根据file*表对文件的处理不同

所以说Linux的做法不是让子进程和父进程共享一张file_struct的表,而是拷贝一份父进程的表,这样不影响进程间的独立性。而执行进程程序替换的时候,不会影响曾经进程打开的重定向文件。因为你替换的是磁盘上拷贝下来的代码和数据,而重定向这些属于内核维护的数据结构,也就是说不影响pcb

Linux下一切皆文件:

OS看来它们都是struct file,没有什么不同,读写方法时直接调用对应的函数指针(多态的思想)。所有的设备和文件,统一都是struct file.

④文件的引用计数

我们close文件,是真的关闭了文件吗?如果真的关闭了文件,那么如果有多个进程打开同一个文件,我关闭了它而别的进程还在使用这样显然是不合适的。所以就有了文件的引用计数,在file结构体中,有一个f_count变量用于统计打开文件的个数。当f_count为0时文件才被关闭,因为当有引用文件,文件不会被关闭。

🏆三、缓冲区的理解

缓冲区本质就是一段内存

缓冲区的意义是节省进程进行数据IO的时间,因为进程将数据存储到磁盘(访问外设)速度很慢,所以就有了进程先将数据放到缓冲区,缓冲区再将数据存储到磁盘。

而存放到缓冲区的数据,缓冲区有自己的刷新策略:

我们来段代码具体感受缓冲区的刷新策略:

 

我们发现在fork子进程之后,当重定位到文件中时,C接口的函数,前后打印了两次,而系统接口前后只打印了一次,这就和缓冲区有关

所以我们知道一个信息,C语言中,存在缓冲区,而write在将数据拷贝到文件中的过程中并不存在缓冲区。但是我们的内核中是存在缓冲区的这个缓冲区存在于将文件中的数据拷贝到磁盘上这个过程中

所以说,如果我们使用C语言将数据拷贝到文件,再存储到磁盘,经历了两次将数据拷贝到缓冲区。而使用OS提供的写入,则只经历了一次将数据存入到内核缓冲区

而内核缓冲区刷新数据到磁盘完全是由操作系统决定的。

那这里也衍生了一个问题,如果操作系统挂掉了,我们在内核缓冲区的数据该怎么办呢?

自己实现的一个迷你版shell命令行控制器

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<stdlib.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<errno.h>
#define NUM 1024
#define OPT_NUM 64

#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)
//do while(0) 包裹一个代码块
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组
int lastCode=0;
int lastSig=0;
int redirType= NONE_REDIR;
char *redirFile=NULL;

void commandCheck(char* commands)
{
  assert(commands);
  char * start=commands;
  char* end=commands+ strlen(commands);

  while(start<end)
  {
    if(*start =='>')
    {
      *start='\0';
      ++start;
      if(*start=='>')
      {
        redirType=APPEND_REDIR;
      }
      else
      {
        redirType=OUTPUT_REDIR;
      }
      //要么是输出要么是追加
      trimSpace(start);
      redirFile=start;
      break;
    }
    else if(*start=='<')
    {
      //拆分成两个
      //"cat < file.txt"
      *start='\0';
      start++;
      trimSpace(start);//
      //
      //usr
      redirType=INPUT_REDIR;
      redirFile=start;
      break;

    }
    else
    {
      start++;
    }
    }
}
int main()
{
  while(1)
  {
    redirType= NONE_REDIR;
    redirFile=NULL;
    //输出提示符
    printf("用户名@主机名 当前路径# ");
    fflush(stdout);
    char *s= fgets(lineCommand,sizeof(lineCommand)-1,stdin);//去除\0
    assert(s!=NULL);
    (void)s;
    //清除最后一个\n,abcd\n  \n重置为\0
    lineCommand[strlen(lineCommand)-1]=0;

    //ls -a -l -i "ls" "-a" "-l" -i"
    //字符串切割

    //"ls -a -l -i" ->"ls" "-a" "-l" "-i"
    commandCheck(lineCommand);



  //strtok
    myargv[0]=strtok(lineCommand," ");
    //如果没有子串了,strtok->NULL,myargv[end]=NULL
    int i=1;
    if(myargv[0]!=NULL&& strcmp(myargv[0],"ls")==0)
    {
      myargv[i++]=(char*)"--color=auto";
    }

    //如果没有子串了,strtok->NULL,myargv[end]=NULL
    while(myargv[i++]=strtok(NULL," "));

    //如果是cd命令,不需要创建子进程,让shell自己执行对应的命令
    if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
    {
      if(myargv[1]!=NULL)chdir(myargv[1]);
      continue;
    }
    if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0)
    {
      if(strcmp(myargv[1],"$?")==0)
      {
        printf("%d,%d\n",lastCode,lastSig);
      }
      else
      {
        printf("%s\n",myargv[1]);
      }
      continue;
    }
    //测试是否成功,条件编译
#ifdef DEBUG
    for(int i=0;myargv[i];i++)
    {
      printf("myargv[%d]: %s\n",i,myargv[i]);
    }
#endif
  //#注释掉DEBUG
  //执行命令
  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(errno);
        }
        //重定向的文件已经成功打开了
        dup2(fd,0);
        }
        break;

      case OUTPUT_REDIR:
      case APPEND_REDIR:
        {
          umask(0);
          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(errno);
          }
          dup2(fd,1);
        }
        break;
      default:
        printf("bug\n");
        break;

    }
    execvp(myargv[0],myargv);
    exit(1);
  
  }
  int status=0;
  pid_t ret= waitpid(id,&status,0);
  assert(ret>0);
  (void)ret;
  lastCode=(status>>8)&0xFF;
  lastSig=(status&0x7F);
 }
    return 0;
}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值