目录
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标准库提供的。
结语
通过这篇博客,我们聊了很多内存中文件相关的知识,不知大家收获如何,那么硬盘中的文件是如何管理的呢?下篇博客文件系统就揭开其神秘的面纱。希望大家给个三连支持一波。