目录
C语言文件操作简单回顾
C语言相关文件接口汇总
默认打开的三个流
系统文件I/O
open
open的第一个参数
open的第二个参数
open的第三个参数
open的返回值
close
write
read
文件描述符
什么是文件描述符
文件描述符分配规则
重定向
重定向的本质
输出重定向 '>'
追加重定向'>>'
输入重定向'<'
dup2函数
添加重定向到自己做的shell中
FILE
用户级缓冲区
结语
C语言文件操作简单回顾
C语言相关文件接口汇总
| 文件的打开和关闭 | |
|---|---|
| fopen | 打开文件 | 
| fclose | 关闭文件 | 
| 文件的顺序读写 | |
| fgetc | 字符输入函数 | 
| fputc | 字符输出函数 | 
| fgets | 文本行输入函数 | 
| fputs | 文本行输出函数 | 
| fscanf | 格式化输入函数 | 
| fprintf | 格式化输出函数 | 
| fread | 二进制输入 | 
| fwrite | 二进制输出 | 
| 文件的随机读写 | |
| fseek | 根据文件指针的位置和偏移量来定位文件指针 | 
| ftell | 返回文件指针相对于起始位置的偏移量 | 
| rewind | 让文件指针的位置回到文件的起始位置 | 
对于相关操作博主就不详细进行演示了,想回顾的可以看下以前写的有关c语言文件操作的博客。
C语言文件操作
对文件进行写入操作
#include <stdio.h>
//写入操作
int main()
{
    FILE* fp = fopen("markdown.txt","w");
    fprintf(fp,"我是写入测试文件\n");
    fclose(fp);
    return 0;
}
对上面的文件进行读取操作:
#include <stdio.h>
int main()
{
    FILE* fp = fopen("markdown.txt","r");
    char buff[256];
    fgets(buff,sizeof(buff)-1,fp);
    printf("%s\n",buff);
    fclose(fp);
    return 0;
}
我们成功将之前写入文件的数据打印到了屏幕上。
文件打开方式总结:
| 文件使用方式 | 含义 | 如果指定文件不存在 | 
|---|---|---|
| “r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 | 
| “w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 | 
| “a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 | 
| “rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 | 
| “wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 | 
| “ab”(追加) | 向一个二进制文件尾添加数据 | 出错 | 
| “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 | 
| “w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 | 
| “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 | 
| “rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 | 
| “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 | 
| “ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 | 
默认打开的三个流
Linux下一切皆文件,在任意进程运行时,都会默认打开三个流,分别为标准输出流(显示屏),标准输入流(键盘),标准错误流(显示器),C语言中用stdout、stdin、stderror来表示。之所以我们能使用printf函数打印结果到屏幕上,其底层实际上就是将我们要打印的内容写入到了标准输出流之中。

C语言的文件操作都是对底层系统调用的封装,在回顾了C语言文件操作后,我们来学习和使用系统文件操作的内容。
系统文件I/O
该部分我们先学会使用相关的系统调用接口,然后通过理解文件描述符,来弄明白重定向的操作。
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);open的第一个参数
第一个参数pathname要打开或创建的目标文件
我们可以以两种方式:
-  以路径的方式给出,会在对应路径下创建和打开该文件。 
-  以文件名的方式给出,会默认在当前路径下创建和打开文件。 
open的第二个参数
第二个参数flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
或运算的奇妙用法:我们可以以对应的比特位是否为0来判断是否要进行对应的操作。
flags参数选项(只列举了常用的六个选项,前三个必须指定一个且只能选择一个):
| O_RDONLY | 只读打开 | 
|---|---|
| O_WRONLY | 只写打开 | 
| O_RDWR | 读,写打开 | 
| O_CREAT | 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 | 
| O_APPEND | 追加写 | 
| O_TRUNC | 有写的权限时,会先清空已存在的文件,之后再写入 | 
传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
#define O_RDONLY         00
#define O_WRONLY         01
#define O_RDWR           02
#define O_CREAT        0100这些选项的二进制序列都只有一个比特位为1(O_RDONLY为0,代表其为默认选项),且各选项比特位为1的位置不同,此时,我们就可以通过或运算将不同的选项组合起来,open内部通过判断对应比特位是否为1就可以知道是否选择了对应的选项了。
//模拟open判断机制
int open(pathname,flags,mode)
{
    if(flags & O_WRONLY) //按位与运算结果为1 ,证明选择了该选项,否则为0
    {
        //TODO
    }
    if(flags & O_RDWR)
    {
        //TODO
    }
    //......  
}open的第三个参数
mode参数表示我们要创建的文件的权限
文件是有权限的

如果我们通过open函数创建文件时没给mode会导致创建的文件的权限是乱码,无法打开该文件。
我们一般以8进制的形式作为mode:

例如,如果将mode设置为0666,文件创建出时的权限应该为:
-rw-rw-rw-但实际上创建的权限为:
-rw-rw-r--这时因为umask(文件默认掩码)的原因,假设默认权限是mask,则实际创建的出来的文件权限是: mask & ~umask 。
umask(0).  //umask默认为2,可以通过指令更改,程序中可以通过umask函数更改如果不需要创建新文件,mode参数可以忽略。
open的返回值
-  成功:新打开的文件描述符(文件描述符的概念后面有讲解,可以先直接跳过去看) 
-  失败:-1 
close
使用close关闭文件
 #include <unistd.h>
 int close(int fd);参数讲解:
-  fd:要关闭文件的文件描述符 
返回值:
-  关闭成功返回0,失败返回-1。 
write
我们使用write向文件中写入信息
//头文件
#include <unistd.h>
//函数原型
ssize_t write(int fd, const void *buf, size_t count);参数讲解:
-  fd: 要写入文件的文件描述符 
-  buf: 存放要写入信息的缓冲区 
-  count:要写入信息的大小 
返回值:
-  如果数据写入成功,返回实际写入数据的字节个数。 
-  如果数据写入失败,返回-1。 
操作示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
    char buff[256];
    int size = snprintf(buff,sizeof(buff)-1,"新建文件log.txt\n");
    buff[size] = 0;
    write(fd,&buff,strlen(buff));
    close(fd);
    return 0;
}
我们创建了一个新文件log.txt,并写入了信息。
read
我们使用read从文件中读信息
//头文件
#include <unistd.h>
//函数原型
ssize_t read(int fd, void *buf, size_t count);参数讲解:
-  fd:要读取文件的文件描述符 
-  buf:读取的数据存放的地方 
-  count:读取数据的大小 
返回值:
-  如果数据读取成功,返回实际读取数据的字节个数。 
-  如果数据读取失败,返回-1。 
读取刚才创建的文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("log.txt",O_RDONLY);
    char buff[256];
    ssize_t size = read(fd,buff,sizeof(buff)-1);
    buff[size] = 0;
    printf("%s\n",buff);
    close(fd);
    return 0;
} 
因为读取的文件里本身就有换行符,所以打印了两次换行。
文件描述符
什么是文件描述符
我们都知道,一个程序要想运行,得先从磁盘加载到内存中,此时操作系统会创建该进程对应的PCB(Linux下是task_struct),进程地址空间(mm_struct),页表等数据结构,之后再通过页表建立虚拟内存和物理内存直接的映射关系。

我们可以通过进程来打开文件,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。内存中有很多打开的文件,那么我们如何对他们进行管理呢?
所以必须让进程和文件关联起来。每个进程PCB中都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

文件描述符分配规则
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    for(int i=0;i<6;++i)
    {
        int fd = open("new file",O_WRONLY|O_CREAT,0666);
        printf("%d ",fd);
    }
    printf("\n");
    return 0;
}
我们从上面可以看到,文件描述符以此从3开始递增分配,为什么是这样呢?
-  首先通过之前的讲解,我们知道了每个进程都会默认打开三个流,0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。 
很明显,文件描述符的分配是从小到大来分配的,前面哪里有空位就分配到哪个。
我们可以试着关闭默认打开的三个流来进行测试。
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    close(0); //关闭标准输入
    int fd = open("new file",O_WRONLY|O_CREAT,0666);
    printf("%d\n",fd);
    return 0;
}
关闭标准输出流:

我们可以看到什么都没打印,为什么?
-  我们的printf函数底层就是把数据写入到标准输出流(即显示器),关闭后所以才什么都没有打印。 
那么数据写到了文件里吗?是的。

1被写到了创建的文件中,而这也就是我们所说的重定向!
重定向
重定向的本质
改变文件描述符0,1所指向的打开的文件

输出重定向 '>'
命令行操作:

代码演示:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    close(1); //关闭标准输入
    int fd = open("newfile",O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("i am new file%d\n",fd);
    fflush(stdout);
    close(fd);
    return 0;
}
追加重定向'>>'
命令行操作:

在该文件原有内容的基础上追加了内容。
代码演示:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    close(1); //关闭标准输入
    int fd = open("newfile",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("i am append data %d\n",fd);
    fflush(stdout);
    close(fd);
    return 0;
}
追加重定向和输出重定向的区别:
-  输出重定向是覆盖式输出数据 
-  而追加重定向是追加式输出数据。 
输入重定向'<'
将从标准输入流中读取改为从文件中读取。
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    close(0);
    int fd = open("newlog.txt",O_RDONLY);  
    char buff[64];
    while(scanf("%s",buff)!=EOF)      //从newlog.txt文件中获取输入
    {
        printf("%s\n",buff);
    }
    close(fd);
    return 0;
}
-  scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是从文件描述符为0的文件(标准输入流)中读取数据。 
dup2函数
像上面那样先关闭对应的文件描述符来实现重定向是很low的,因此还提供了名为dup2的系统调用。
原理:
把新打开文件的fd拷贝覆盖到指定fd下

 //头文件
 #include <unistd.h>
 //函数原型
 int dup2(int oldfd, int newfd);-  dup2 会把 arry[oldfd]的内容拷贝到arry[newfd]中 
参数讲解:
-  oldfd: 要进行拷贝的fd 
-  newfd: 将被覆盖的fd 
返回值:
-  如果调用成功,返回newfd,否则返回-1。 
改造下前面的代码:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h> 
#include <sys/stat.h>
#include <unistd.h>
int main()
{
    printf("\n");
    int fd = open("newfile",O_WRONLY|O_CREAT,0666);
    dup2(fd,1);
    printf("i am new file%d\n",fd);
    fflush(stdout);
    close(fd);
    return 0;
}
一样实现了之前的效果。
添加重定向到自己做的shell中
在进程控制部分时,我们写了一个自己的shell,这次我们将重定向功能也添加到其中。
Linux\进程控制精讲,简单实现一个shell_Sola一轩的博客-CSDN博客
如何添加重定向功能?
-  检测是否需要重定向,确定是哪种重定向,记录下对应的状态 
-  获取重定向符号后的文件名 
-  使用对应文件打开方式打开文件后,使用dup2函数 
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 1024
char CommandLine[SIZE];  //存放输入的指令
#define OPT_NUM 64
char* Myargv[OPT_NUM];  //存放分割后的程序指令
//保存上次运行时的退出码和退出信号
int lastCode;
int lastSignal;
#define NOREDIR 0
#define INREDIR 1   //输入重定向
#define OUTREDIR 2  //输出重定向
#define APPREDIR 3  //追加重定向
int RedirMode = NOREDIR;         //重定向的模式
char* Redirfile = NULL;  //存储要重定向的文件名
#define RmSpace(start) do{ \
 while(isspace(*start)) ++start;\
} while(0)                 //定义宏函数
void CommandCheck(char* cl)  //检测是否需要重定向
{
    assert(cl!=NULL);
    char* start = cl;
    char* end = cl+strlen(cl);
    while(start < end)
    {
        if(*start == '>')  //输出重定向
        {
          *start = '\0';
          ++start;
          if(*start == '>')  //追加重定向
          {
            RedirMode = APPREDIR;
            start++;
          }
          else
          RedirMode = OUTREDIR;
          //去掉空格
          RmSpace(start);
          Redirfile = start;       //获取文件名
          break;
        }
        if(*start == '<')   //输入重定向
        {
          *start = '\0';
          RedirMode = INREDIR;
          ++start;
          RmSpace(start);
          Redirfile = start;
          break;
        }
        ++start;
    }
}
int main( )
{
  while(true)
  {
    RedirMode = NOREDIR;
    Redirfile = NULL;
    //1.打印提示符
    printf("[用户名@主机名 当前路径]#");
    fflush(stdout);        //刷新缓冲区
    //获取用户输入
    char* s = fgets(CommandLine,sizeof(CommandLine)-1,stdin);
    assert(s != NULL);  //检查释放获取成功
    (void)s;      
    CommandLine[strlen(CommandLine)-1] = 0;  //消除掉输入时带的换行符
    CommandCheck(CommandLine);
    //字符串分割,拿出指令
    Myargv[0] = strtok(CommandLine," ");
    int i = 1;
    //给ls命令增加配色方案
    if(Myargv[0]!=NULL && strcmp(Myargv[0],"ls")==0)
    {
      Myargv[i++] = (char*)"--color=auto";
    }
    while( Myargv[i++] = strtok(NULL," "));  //无法分割时返回空指针。 命令行参数最后刚好需要以NULL结尾
    //内建命令,内置命令不需要创建子进程来执行
    //cd 命令需要改变当前进程的工作目录
    if(Myargv[0]!=NULL && strcmp(Myargv[0],"cd")==0)
    {
        if(Myargv[1]!=NULL)
        chdir(Myargv[1]);
        continue;
    }
    //echo命令获取上次程序的退出码
    if(Myargv[0]!=NULL && Myargv[1]!=NULL && strcmp(Myargv[0],"echo")==0)
    {
        if(strcmp(Myargv[1],"$?")==0)
        {
          printf("lastcode:%d , lastSignal:%d\n",lastCode,lastSignal);
        }
        else
        {
          printf("%s\n",Myargv[1]);
        }
        continue;
    }
//条件编译来测试  编译时带上 -DDEBUG即可运行测试
#ifdef DEBUG
for(int i=0; Myargv[i] ;++i)
printf("%s\n",Myargv[i]);
#endif
 //创建子进程执行相关指令
pid_t id = fork();
assert(id != -1); //检测子进程是否创建失败
if(id == 0) //子进程进程切换 执行对应的指令
{
  switch(RedirMode) 
  {
    case NOREDIR:      //什么都不做
    break;
    case INREDIR:    //输入重定向
    {
      int fd = open(Redirfile,O_RDONLY);
      dup2(fd,0);
    }
    break;
    case OUTREDIR:
    case APPREDIR:
    {
       int flags = O_CREAT | O_WRONLY ;
      if(RedirMode == OUTREDIR)
      {
        flags |= O_TRUNC;
      }
      else
      {
        flags |= O_APPEND;
      }
    int fd = open(Redirfile,flags,0666);
    dup2(fd,1);
    }
    break;
    default:
    printf("未知错误\n");
    break;
  }
  execvp(Myargv[0],Myargv);
  exit(1); //异常时才从这退出
}
int status;  //拿到子程序的退出码
waitpid(id,&status,0);
lastCode = ((status>>8) & 0xFF);
lastSignal = (status & 0x7F);
    
 }
  return 0;
}

FILE
-  因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 
-  所以C库当中的FILE结构体内部,必定封装了fd。 
在/usr/include/stdio.h中,我们可以看到这句代码:
typedef struct _IO_FILE FILE;很明显FILE是struct _IO_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
};从上我们可以看到其内部封装了文件描述符,并且FILE还有自己的缓冲区。
用户级缓冲区
来段代码感受一下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    const char* st1 = "hello printf\n";
    const char* st2 = "hello fwrite\n";
    const char* st3 = "hello write\n";
    printf("%s",st1);
    fwrite(st2,strlen(st2),1,stdout);
    write(1,st3,strlen(st3)); 
    fork();
    return 0;
}
接着我们进行重定向:

我们发现 printf 和 fwrite(库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
-  一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。 
-  printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。 
-  而我们放在缓冲区中的数据,就不会被立即刷新,甚至在fork之后 
-  但是进程退出之后,会统一刷新,写入文件当中。 
-  但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。 
-  write 没有变化,说明没有所谓的缓冲 
printf 、fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。
-  另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 
-  那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf、fwrite 有,足以说明,该缓冲区是二次加上的,是由C标准库提供的。 
结语
通过这篇博客,我们聊了很多内存中文件相关的知识,不知大家收获如何,那么硬盘中的文件是如何管理的呢?下篇博客文件系统就揭开其神秘的面纱。希望大家给个三连支持一波。



















