C语言文件操作,linux文件操作,文件描述符,linux下一切皆文件,缓冲区,重定向

时间:2024-03-05 17:12:57

目录

C语言文件操作

如何打开文件以及打开文件方式

读写文件

关闭文件

Linux系统下的文件操作

open

宏标志位

write,read,close,lseek接口

什么是当前路径?

linux下一切皆文件

文件描述符

文件描述符排序

C语言文件操作它们究竟是如何找到文件的呢?

重定向

缓冲区


首先我们抛出一个问题——如何进行文件操作?最普遍的方式就是使用我们的高级语言如C,C++,java这样的语言级别的接口来辅助我们操作文件,对文件进行读写操作;那语言级别的接口是怎么进行文件操作的呢?这些语言级别的文件操作为什么可以做到对文件的增删查改呢?

我们先看一下,我们之前C语言阶段学习过的文件操作;

C语言文件操作

我们要进行文件操作首先要做的就是打开我们的文件;那如何打开文件呢?

如何打开文件以及打开文件方式

我们可以使用(常用)fopen这个接口来打开我们的文件,path是我们文件的路径名字,mode是打开这个文件的方式有很多不同的选项;

 文件路径:我们的路径可以是相对路径也可以是绝对路径,这里的相对路径相对的是我们可执行程序运行变成进程时所在的目录;

选项:我们的选项大致有6个,w,w+,r,r+,a,a+,这六个选项代表着我们的6种不同的打开文件的方式,下面的对这六种种方式的简介:(这是man手册中的官方简介)

下面是gpt对这6个选项的总结:

1. "r"(只读模式):用于只读操作。如果文件不存在,则打开操作失败,并返回NULL。如果成功打开文件,则文件指针会被放置在文件的开头。

2. "w"(写入模式):用于写入操作。如果文件不存在,则创建一个新文件。如果文件已存在,则会清空文件中的内容。文件指针会被放置在文件的开头。

3. "a"(追加模式):用于追加操作。如果文件不存在,则创建一个新文件。如果文件已存在,则文件指针会被放置在文件的末尾。新写入的内容会追加到文件的末尾,不会清空原有内容。

这三种基本的打开模式还可以与"+"符号结合使用,表示具有读写权限:

1. "r+"(读写模式):用于读写操作。文件指针会被放置在文件的开头。可以对文件进行读取、修改和写入操作。

2. "w+"(读写模式):用于读写操作。如果文件不存在,则创建一个新文件。如果文件已存在,则会清空文件中的内容。文件指针会被放置在文件的开头。可以对文件进行读取、修改和写入操作。

3. "a+"(读写模式):用于读写操作。如果文件不存在,则创建一个新文件。如果文件已存在,则文件指针会被放置在文件的末尾。可以对文件进行读取、修改和写入操作。

需要注意的是,在使用读写模式时,要特别小心文件指针的位置以避免错误的读取或写入操作。使用`fseek()`函数可控制文件指针的位置。

我再用我的方式总结一下:

r和w代表的就是只读只写 流(也可以看作我们打字时候的光标位置)位于文件开头,不同的是w会清空文件所有内容,并且当文件不存在时会自动创建一个文件提供给用户,而r不具备创建和消除的功能它只能读而且是从开头读;

r+和w+的区别与r和w的区别一样(创建和消除的区别)但是我们需要注意的是r+和w+它们在读写的时候流是会随着读写的位置移动的所以我们在写或者读的时候要注意流的位置,就比如我们使用r+我们先写入了一段数据我们想要读我们这个文件中所有数据的时候我们一定要把流的位置移动到文件开头不然读到的数据就是我们写入数据的后面的位置因为我们写入数据到了这里所以流的位置变了不是在文件开头了;

a和a+是以追加的形式打开文件文件不存在则创建存在则将流放置在文件结尾,a+可以读文件而a不可以;

接下来我们说说读写文件的函数:

读写文件

读:fscanf,fread, fgets

写:fprintf, fwirte, fputs

这些读写的函数就不多进行描述了在man手册中都可以清楚明了的看到它们的内容以及如何使用;

我提供一个我的记忆方式:就是和我们C语言中的gets,scanf,puts,printf一起记忆,scanf和printf只是在参数前面多了一个流参数,而fputs是在puts参数后面多了一个流,而fgets只是在gets函数的参数后面多了一个存放文件中数据的数组大小和流;(因为fgets字符串类型函数所以我们只需要提供数组大小即可fgets会自动帮我们在数组末尾加\0的)

下面是读写文件的操作示例:

关闭文件

我们打开了文件之后自然需要关闭文件,这样可以保证文件的安全性等;

fclose就是关闭文件的函数,使用也非常简单把FILE类型的指针给fclose就可以关闭文件了;

上面的就是C语言文件操作的简单复习了;

Linux系统下的文件操作

接下来我们进入重点,为什么高级语言可以对文件进行操作呢?它到底是怎么做到的?

首先,我们回顾一下我们之前的对操作系统的理解,操作系统是为进程提供安全稳定环境的软件,它负责管理我们计算机中的进程,我们使用C语言编写程序的时候所用到的软件以及库函数其实都是操作系统提供的,而我们的C语言的库函数就是在系统接口的基础上对库函数进行封装带上一系列的自定义行为,这样既可以让我们的代码跨平台服务也简化了我们的语言学习与使用;

既然C语言库函数的底层是系统调用那么C语言文件操作库函数的底层的系统调用是什么样的呢?我们下面来看看在linux下C语言的文件操作的系统调用接口;

open

对应着库函数中的fopen,open有两类函数一类是三个参数的一类是两个的;两个参数的open函数一般是只用来读文件,三个参数的函数则是我们常用的函数;

open的三个参数

第一个参数代表了我们的文件路径

第二个参数是打开文件的选项,选项常用的有:

O_RDONLY:只读

O_WRONLY:只写

O_RDWR:可读可写

上面这三个选项必须有且只有一个选项在参数中,这些参数是不是和C语言文件操作函数中的r w +选项类似,是的!因为上面的参数就是C语言选项的底层;再加上下面这几个选项就可以成功的组建出C语言文件操作函数的打开功

O_CREAT:文件不存在时通过前面的路径创建出一个文件

O_APPEND:在文件末尾追加

O_TRUNC: 清空文件

但是这么多的选项我们怎么样才能在一个参数中使用呢?这个时候就是宏标志位登场的时候啦!我们首先看看上面的选项是不是都是大写?大写的字符在C语言中一般是不是在宏定义的时候经常使用呢?

那什么是标志位?顾名思义就是位上不同的信号,一个int类型数据有32个bit位在每个位上可以有0,1两种信号,所以我们可以使用一个参数来传递32种不同的选项,某位上为0则是否为1则代表选项成立;

宏标志位

我们可以通过下面这个小程序来理解宏标志位:

#include<stdio.h>
通过宏标志位来传递参数
#define FIRST 0x1
#define SECOND 0x2
#define THIRD 0x4

void fun(int macro)
{
  if(macro&FIRST)
    printf("这是功能1\n");
  if(macro&SECOND)
    printf("这是功能2\n");
  if(macro&THIRD)
    printf("这是功能3\n");
}

int main()
{
  fun(1);//0001
  printf("-------------------\n");
  fun(2);//0010
  printf("-------------------\n");
  fun(3);//0011
  printf("-------------------\n");
  fun(4);//0100
  printf("-------------------\n");
  fun(5);//0101

  return 0;
}

现象: 

可以看到我们通过宏标志位的传递实现的多种不同功能的组合使用;

接下来我们看第三个参数

第三个参数是用来修改我们文件权限的,这也是为什么我们常使用的是三个参数的open函数的原因;当我们不使用低三个参数的时候我们会发现:

为了避免这种情况我们使用第三个参数;

我们通过传递权限的八进制数来修改权限,这里可以看我前面写的博客来理解

Linux操作系统——权限-CSDN博客https://blog.csdn.net/m0_75260318/article/details/132946078?spm=1001.2014.3001.5501所以我们传递需要这样来操作

 这就是open函数的使用;

write,read,close,lseek接口

write:

第一个参数是文件描述符,就是我们open打开后的返回值具体是什么,我们下面的文件描述符会详细讲解;

第二个参数就是我们的写入数据的内容,我们可以传数组,也可以传字符串;

最后一个参数就是字节数,是我们传递的数据的字节数;

write接口会向我们的内核中写入我们的buf数据,再通过内核写入到fd文件中;

read:

它的参数和上面的write没有什么不同,不过是将数据从fd文件中读取count个字节的数据到buf中;

第二个参数是我们接受数据的指针,指向存储数据的区域;

close:

关闭打开的fd文件

lseek:

用来调整文件流的位置(光标位置)

下面是我通过chat-gpt获得的关于lseek的信息:

- `fd` 是已经打开的文件的文件描述符,可以使用 `open` 函数获取。
- `offset` 是要移动的偏移量,可以为正(向文件尾部移动)或负(向文件开头移动)。
- `whence` 是指定偏移量是相对于文件开头、当前位置还是文件尾的标志。
  - 如果 `whence` 为 `SEEK_SET`,`offset` 表示文件开始位置的偏移量。
  - 如果 `whence` 为 `SEEK_CUR`,`offset` 表示当前的位置偏移量。
  - 如果 `whence` 为 `SEEK_END`,`offset` 表示文件末尾的偏移量。

什么是当前路径?

为什么我们可以把文件创建在进程当前路径中,当前路径到底是什么意思?我们在运行可执行程序后程序变成了进程,而进程在我们之前的学习中,我们知道进程有进程的pcb结构体,这个结构体中存放了进程的各种信息,那么我们运行进程时所在的路径也会被记录下来,这个路径就是当前进程所在的路径,也就是当前路径的根本意义;

下面我们通过实际现象来证明我们思路:

由此我们可以清楚的明白所谓的当前路径就是存储在进程的pcb结构体中的进程信息,我们只要通过查找进程的结构体就可以找到我们的当前路径; 

我们讲完了open的参数接下来我们得看看open的返回值,我们可以惊讶的看到open这个系统调用的返回值居然是int类型的,这说明它只返回了一些数字,而返回数字就和我们前面C语言封装函数fopen的接口有很大差别了,C语言接口返回的是一个指针类型,至少还是可以指向某个大的存储区域的,而我们这里简简单单返回一个int究竟做了什么呢?我们就要引入一个新的知识——文件描述符;

linux下一切皆文件

我们先回顾一下Linux下一切皆文件这个概念;之前的学习中我们模模糊糊知道了linux下一切皆文件,但究竟什么叫一切皆文件呢?我们的计算机是由硬件和软件组成对于我们的操作系统,操作系统把硬件一般叫做外设,而操作系统需要管理外设,来为我们用户提供服务;如何管理呢?先描述,在组织;外设无法直接与操作系统交流,于是外设的信息形成了一个个结构体,操作系统使用不同的数据结构将这些结构体组织起来,并对结构体中的信息进程增删查改来管理外设;而要对这些外设进行访问肯定要使用到open,write,read这样的接口;

通过上面的解释我们就可以很好的理解linux下一切皆文件的道理;

文件描述符

上面讲到open系统接口的返回值就是文件描述符,并且write,read,close,lseek这样的系统接口的参数都需要用到文件描述符,那么文件描述符究竟是什么;之前我们说linux下一切皆文件,所以一切的文件被打开的文件都会有包含文件属性结构体file,file中存储了文件的相关信息,

而这些文件的结构体又由打开它们的进程进行管理,进程的pcb结构体中有一个files*的指针这个指针指向了文件结构体(这个结构体装的是进程中被打开的所有文件的信息),

在files_struct结构体中又有一个file*的fd_array指针数组(也叫文件映射表)这个数组装的就是file结构体的指针,这些指针就是指向被打开文件的;

文件描述符排序

而我们的linux进程默认打开0标准输入(对应外设键盘),1标准输出(显示器),2标准错误(显示器)三个文件(外设),而数组的0,1,2三个位置就是指向这三个文件的;在C语言中0,1,2被封装成了stdin,stdout,stderr;剩下的我们自己打开的文件会跟随我们的文件的打开顺序来分配数组下标;

需要注意的是:

如果我们关闭了文件,那么文件所在的fd_array数组位置将会制空,为接下来的打开文件让开位置;

了解了这些,现在我们总算是知道了文件描述符fd究竟是什么了,文件描述符其实就是数组下标而已,我们可以通过数组下标找到,数组中的指针,通过指针找到文件信息结构体,从而找到文件;

C语言中的文件描述符fileno:由此可见C语言的FILE结构体中一定也封装了fd;事情也是如此,C语言的FILE结构体中有fileno这样的成员变量,这个成员变量就是我们系统接口中的文件描述符;

C语言文件操作它们究竟是如何找到文件的呢?

如fopen这样的函数,首先它肯定是先调用我们的系统接口open,open接口又通过进程的结构体中的files指针找到files_struct结构体,在结构体中的fd_array数组中对应封装的fileno编号,通过编号对应的数组下标找到数组中指针指向的文件结构体,从而找到文件,打开文件;这就是C语言的文件操作找到文件的全部过程;(对照上面的图片分析效果更佳)

重定向

由于linux进程默认打开了三个文件,所以我们的输入输出默认也会从我们的键盘和屏幕读写;而重定向的意思就是改变默认的读写文件;就比如我们的cat AA.txt>BB.txt,这个指令就是把AA.txt中的数据输出到BB.txt文件中,本来cat指令默认是输出到显示器上的但是由于重定向>使得输出的方向变了;

我们可以使用dup2函数进行重定向:

这个函数的作用是将fd_array文件映射表上的oldfd下标位置的指针拷贝给newfd位置的指针;

示例

重定向有输出重定向> 输入重定向<  追加重定向>> 这些重定向;(这里就不一一实现了)

缓冲区

缓冲区这一概念我们一定都很熟悉,但是我以前只知道如果输入或者输出的字符没有打印完全的话,就刷新一下缓冲区就好了,比如使用fflush(stdout)或者使用getchar读取输入缓冲区中的\n字符;这样的理解有些片面,所以现在我们来理想的面对一下缓冲区;

缓冲区是什么?

其实缓冲区就是一段内存空间,用来存储我们的数据;

缓冲区有什么用?

当没有缓冲区的时候,我们的内核之间向外设写入,每次有数据就直接写入外设这样的写透模式WT,伴随着多次的交互,有交互就一定需要外设的准备,准备就一定需要消耗时间,并且我们的各级内存之间的运行速度差距很大,消耗的时间一定更多,而多次交互就要多次消耗时间(虽然这个时间在我们看来很多但对应计算机来说太浪费时间了),为了减少内核和外设的交互次数,就出现了缓冲区这一概念;将数据放入缓冲区当缓冲区满了或者满足刷新策略的时候就刷新缓存区将数据输出;这样一来交互的次数大大减少了!

缓冲区在哪?

缓冲区其实就是一段定义好的buffer数组,我们的C语言库中有着buffer的实现,如我们使用scanf函数的时候就是先把数据写入buffer中当遇到换行时就会和内核交互将数据刷新入内核,内核也有自己的缓冲区也有自己的刷新模式;在C语言中向显示中刷新是行刷新策略向磁盘刷新一般是满刷新策略;所以用户级缓冲区在用户使用的高级语言的库中实现,而内核级缓冲区在内核的代码中实现;

模拟实现缓冲区

#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define BUFFER_SIZE 1024
#define BUFFER_SIZE 10

typedef struct MyFILE
{
  char _buffer[BUFFER_SIZE];
  int _end;
  int _fd;
  int _end;//指向最后一个数据后面一个也可以当作是size
  int _fd;//文件描述符
}file;

file* myfopen(const char*filename,const char* mode)
@@ -68,56 +68,71 @@ void myfflush(file*stream)
  }
  write(stream->_fd,stream->_buffer,stream->_end);//载入内核缓冲区
  syncfs(stream->_fd);//载入磁盘
  stream->_end=0;
  memset(stream->_buffer,0,sizeof(stream->_buffer));
}

void myfputs(const char*str,file*stream)
{
  file*fp=stream;
  if(fp->_end+strlen(str)>=BUFFER_SIZE)
  if(fp->_end+strlen(str)>=BUFFER_SIZE)//满刷新判断,这是所以文件都需要满足的
  {
    //如果满了直接就载入内存了
    myfflush(fp);
    fp->_end=0;
    memset(fp->_buffer,0,BUFFER_SIZE);
    fprintf(stderr,"范围那里错了\n");
    while(strlen(str)>=BUFFER_SIZE)//如果刷新了之后还是满了的话
    {
      memcpy(fp->_buffer,str,BUFFER_SIZE-1);
      fp->_end=BUFFER_SIZE-1;
      stream->_buffer[fp->_end]='\0';
      str=str+BUFFER_SIZE-1;
      myfflush(fp);
    }
    //不再满了之后
    strcpy(fp->_buffer,str);
    fp->_end=strlen(str);
    //debug
    
    //printf("%s\n",fp->_buffer);
  }
  else{//不满则直接放入缓冲区
    strcpy(fp->_buffer+fp->_end,str); 
    fp->_end+=strlen(str);
  }


  strcpy(fp->_buffer+fp->_end,str); 
  fp->_end+=strlen(str);
  if(fp->_fd==1||fp->_fd==2)//如果是标准输出和标准错误时的刷新策略
  { 
    //标准输出
  
    //标准输出和标准错误
    for(int i=strlen(str)-1;i>=0;i--)
    {
    {//从后往前寻找如果有\n就刷新
      if(str[i]=='\n')
      {
        write(fp->_fd,fp->_buffer,i+1);
        if(str[i+1]!='\0')
        myfflush(fp);
        if(str[i+1]!='\0')//把\n后面没有刷新的重新放入缓冲区
        {
          fprintf(stderr,"我遇到\n");
          strcpy(fp->_buffer,fp->_buffer+(i+1));
          strcpy(fp->_buffer,str+i+1);
          fp->_end=strlen(fp->_buffer);
        }
        return;
      }
    }
  }
}

void myfclose(file *stream)
{
  myfflush(stream);
  close(stream->_fd);
}
int main()
{
  close(1);
  file *fp=myfopen("tmp.txt","w");
  fprintf(stderr,"%d\n",fp->_fd);
  myfputs("hello myfputs\n",fp);

  printf("fp :%d\n",fp->_fd);
  char str[]="12345678912345678910\n";
  myfputs(str,fp);
  myfclose(fp);
  return 0;
}

这是我模拟实现缓冲区的代码,可以辅助理解;