> 文章列表 > 【Linux】基础IO,详解系统文件IO

【Linux】基础IO,详解系统文件IO

【Linux】基础IO,详解系统文件IO

目录

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要打开或创建的目标文件

我们可以以两种方式:

  1. 以路径的方式给出,会在对应路径下创建和打开该文件。

  2. 以文件名的方式给出,会默认在当前路径下创建和打开文件。

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博客

如何添加重定向功能?

  1. 检测是否需要重定向,确定是哪种重定向,记录下对应的状态

  2. 获取重定向符号后的文件名

  3. 使用对应文件打开方式打开文件后,使用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++;}elseRedirMode = 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 0int _blksize;
#elseint _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标准库提供的

结语

        通过这篇博客,我们聊了很多内存中文件相关的知识,不知大家收获如何,那么硬盘中的文件是如何管理的呢下篇博客文件系统就揭开其神秘的面纱。希望大家给个三连支持一波。