文件
在之前学习C
语言文件操作时,我们了解过什么是文件,这里简单回顾一下:
文件存在磁盘中,文件有分为程序文件、数据文件;二进制文件和文本文件等。
详细描述见文章:文件操作——C语言
- 文件在磁盘里,磁盘是永久性存储介质,因为文件在磁盘上的存储是永久性的;
- 磁盘是外设(是输入设备也是输出设备);
- 本质上对磁盘是文件的所有操作,都是对外设的输入和输出;也就是
I/O
。
对于文件的认识:
- 文件 = 文件内容 + 文件属性
- 文件大小是
0
KB的文件也是占用磁盘空间的- 所有对于文件的相关操作都是对文件内容和文件属性就行操作。
在系统层面理解
- 我们操作文件(打开,关闭文件),本质上是进程对于文件的操作;
- 磁盘的管理者是操作系统;
- 我们在C/C++使用库函数来对文件进行读写操作,本质上是通过文件相关的系统调用去完成的。
C
文件操作
打开和关闭文件
在C
语言当中,我们通过fopen
来打开文件,fclose
来关闭文件;
fopen
:打开文件,如果打开成功返回一个FILE*
类型的指针,执行被打开的文件;打开失败则返回NULL
;fclose
:关闭文件,传参FILE*
类型的指针,关闭指定文件。
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以读方式打开,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; } //.... fclose(fp);//关闭文件 return 0;
}
打开文件的方式
我们知道在C
语言的fopen
接口打开文件是有很多打开方式
r
:以读方式打开、w
以写方式打开、a
以追加方式打开。
r
方式,当文件不存在时就打开文件失败;
w
方式,当文件不存在时就新建文件(在当前工作路径下创建,进程
当中存放了当前工作路径);如果文件存在会清空当前文件的内容;然后在进入写入。
a
方式,追加,当文件不存在就新建文件;如果文件已经存在,打开时不会清空文件内容,而是在文件末尾进行写入
写文件
当我们以w
/r
方式打开一个文件,我们要将内容写到文件当中;
我们可以使用fputc
、fputs
、fwrite
、fprintf
进行文件的写入;
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以读方式打开,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; } for(char ch = 'a';ch <= 'z';ch++){ fputc(ch,fp); } char* str = (char*)"I love you\n"; fputs(str,fp); int x = 100; fprintf(fp,"x = %d\n",x); fclose(fp);//关闭文件 return 0;
}
文件读取
我们以r
方式打开一个文件,我们要像读取这个文件的内容,我们可以使用fgetc
、fgets
、fscanf
进入文件内容的读取操作:
#include <stdio.h>
int main()
{ FILE* fp = fopen("log.txt","w");//以读方式打开,文件不存在就新建 if(fp == NULL){ perror("fopen"); return 1; }for(int i = 0;i<26;i++){ printf("%c",fgetc(fp)); } printf("\n"); char buff[20]; fgets(buff,12,fp); //buff[11] = '\0'; printf("%s",buff); int x; fscanf(fp,"x = %d",&x); printf("x = %d\n", x); fclose(fp);//关闭文件 return 0;
}
stdin/stdout/stderr
在我们程序运行时,C
语言它会默认打开三个文件:stdin
、stdout
和stderr
;
我们可以发现这三个都是文件类型指针
系统文件I/O
在上述C
语言的文件操作fopen
、fclose
都是语言层提供给我们的文件操作接口;以及C
语言的stdin
、stdout
、stderr
;C++中的cin
、cout
、cerr
都是语言层提供给我们的方案;
我们知道文件的管理者是操作系统,所以我们对文件操作都要经过操作系统;
那也就是说语言层的文件操作接口,其底层都封装了系统调用。
1. 传递多个标志位的方法
在之前的习惯中,我们通常使用一个参数来作为一个标记位;这样我们在传递多个标志位时就需要传递多个参数。
现在来了解一种使用一个参数来传递多个标志位的方法:
使用一个
bit
为来作为一个标识符,这样使用一个参数就可以表示多个标志位了。
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flag){ if(flag & ONE) printf("ONE "); if(flag & TWO) printf("TWO "); if(flag & THREE) printf("THREE "); printf("\n");
}
int main()
{ func(ONE); func(ONE | TWO); func(ONE | THREE); func(ONE | TWO | THREE); return 0;
}
这样就可以使用一个参数,传递多个标志位了。
Linux
操作系统open
接口就使用了一个参数来传递多个标志位。
2. 打开文件open
在语言层面,我们使用的fopen
,它本质上就是对系统调用的封装;
可以看到
open
是一个系统调用;它的作用就是打开一个文件,也可能会创建一个新的文件。
我们可以看到open
函数它存在一个两个参数的,也存在一个三个参数的;
pathname
:表示要打开文件的文件名(不带路径就默认在当前工作路径下)flags
:表示文件的打开方式,存在多个标志位mode
:表示新建文件时,文件的默认权限
文件名pathname
这个想必就不用多说了,表示要打开文件的文件名;
不带路径时,就表示在当前工作路径下打开文件。(进程中存在当前工作路径cwd
)
标志位flags
通过查看open
函数说明可以看到,flags
存在非常多的标志位;这里列举一些常用的选项
O_RDONLY
只读、O_WRONLY
只写、O_RDWR
读写;
O_CREAT
:文件不存在时就新建文件
O_TRUNC
:打开文件时,文件存在就清空文件内容
O_APPEND
:打开文件时,以追加形式打开。
这里flags
表示的就是文件的打开方式;
首先就是:只读、只写和读写;在我们打开文件时必须指定一个且只能指定一个。
O_CREAT
在我们打开一个文件时,如果这个文件不存在,那open
函数就会直接返回-1
表示文件打开失败;
而我们带了O_CREAT
选项,就表明当文件不存在时,就新建文件;(这里新建文件要指明新建文件的权限,否则创建出来文件的权限就是乱码)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY,0666); if(fd < 0){ perror("open"); return 1; } return 0;
}
一般情况下,在以写方式打开文件,文件不存在就新建,就要指明文件的权限。
(以只读读方式
O_RDONLY
,文件不存在新建出的文件是空的,没有什么意义)
O_TRUNC
当我们打开一个文件时,如果这个文件已经存在了,那就打开这个已有的文件;
如果我们带了O_TRUNC
选项,就表示清空这个文件的内容;
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); if(fd < 0){ perror("open"); return 1; } return 0;
}
O_APPEND
在C语言的文件操作中,fopen
打开文件,w
就是以写方式打开、文件不存在就新建、文件存在就清空文件的内容;(这就对应了上述选项的O_WRONLY
、O_CREAT
、O_TRUNC
)
但是我们fopen
还可以以a
方式打开文件,也就是追加方式;这里的O_APPEND
就是以追加的方式打开文件。
这里我们先看一种现象:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY,0666); if(fd < 0){ perror("open"); return 1; }char buff[] = "abcdef";write(fd,buff,strlen(buff)); return 0;
}
可以看到,我们不带
O_APPEND
选项,写入的时候是在文件的开头位置进行写入的。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("love.txt",O_CREAT | O_WRONLY | O_APPEND,0666); if(fd < 0){ perror("open"); return 1; }char buff[] = "abcdef";write(fd,buff,strlen(buff)); return 0;
}
文件权限mode
当打开文件,文件不存在,我们还带了O_CREAT
选项时;如果我们不带文件的权限,那新建文件的权限就是乱码;
而新建的文件的权限:文件权限 = 默认权限 &(~umask)
3. 关闭文件
关闭文件的系统调用close
,根据文件描述符,关闭指定文件。
对于这里的
fd
,它指的是文件描述符,当我们打开文件成功时,会返回给我们该文件的文件描述符;我们对指定文件的读写操作,以及关闭文件都要使用指定文件的文件描述符。
4. 文件写入write
在C语言中,我们进行文件写入时,我们有非常多的接口;
但是在系统层面,我们对文件进行写入只有一个接口,就是write
。
write
有三个参数,分别是fd
、buf
、和count
;
其中
fd
指的是文件描述符,表示我们要向哪一个文件进行写入;
buf
表示我们要进行写入的内容,它是void*
类型的指针,可以写入任何数据
count
就表示,我们要写入内容的长度。
这里值的注意的是:这里write
主要用于字符写入
当然,我们之前听过文本写入和二进制写入,那都是语言层的概念;
再这里我们操作系统不管这些,
write
就将给定的内容写入到文件缓冲区中。
5. 文件读取read
和写入一样,在语言层我们有非常多的函数接口,但是在系统层面,就只有read
。
这样存在三个参数;
fd
文件描述符,表示要从哪一个文件读取数据;
buf
,表示我们要将文件中的数据读取到buf
中;
count
表示要读取内容的长度。
文件描述符
在上面描述中,我们了解了文件操作相关的系统调用;
在open
、write
、read
、close
这些系统调用中,都用到了一个fd
来指明一个文件;那这个fd
究竟是什么呢?
open
函数的返回值
在上述使用open
函数时,我们并没有关心open
函数的返回值,也没有说明文件描述符到底是什么?
当我们使用
open
打开一个文件时:如果打开成功,那就返回这个新打开文件的文件描述符;
如果打开失败,就返回
-1
,并且设置错误码。
什么是文件描述符
那我们知道了,文件描述符就是标识一个文件的整数,每一个文件的文件描述符都不一样;
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);if(fd < 0) return -1;printf("fd : %d\n",fd);close(fd);return 0;
}
我们可以看到打开的一个文件的文件描述符是3
;
这里打开一个文件我们看不出来什么,我们多打开一些文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{ int fd1 = open("log1.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd2 = open("log2.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd3 = open("log3.txt",O_CREAT | O_WRONLY | O_APPEND,0666);int fd4 = open("log4.txt",O_CREAT | O_WRONLY | O_APPEND,0666);//这里就不做错误判断了printf("fd1 : %d\n",fd1);printf("fd2 : %d\n",fd2);printf("fd3 : %d\n",fd3);printf("fd4 : %d\n",fd4);//不关闭文件,进程结束会自动关闭return 0;
}
这里,我们可以发现我们打开多个文件,这些文件的文件描述符是线性增长的。
文件描述符0/1/2
在上述中,我们发现,我们的文件描述符它是线性增长的,而且我们打开第一个文件它的文件描述符是3
;
那0
、1
、2
文件描述符去哪里了呢?
还记得在程序运行时,
C
语言会默认给我们打开三个文件stdin
、stdout
和strerr
;在
Linux
操作系统中,进程默认情况下会有三个缺省打开的文件描述符(0
、1
、2
),分别是标准输入、标准输出和标准错误。那这里进程默认打开的标准输入、标准输出和标准错误和C语言中的
stdin
、stdout
和stderr
有什么区别呢?
这里0
标准输入、1
标准输出、2
标准错误;
一般情况下0,1,2
对应的物理设备是键盘、显示器、显示器。
所以,我们从键盘中读取数据就是从0
文件中读取数据;将数据输出到显示器中就是将数据输出到1
文件当中。
文件描述符的分配规则
当我们打开一个文件时,它的文件描述符是3
;我们打开多个文件时,我们可以发现它的文件描述符是线性增长的;
那文件描述符是如何分配的呢?是一直在增长的吗?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{ int fd1 = open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd1 < 0) return -1; printf("fd1 : %d\n",fd1); close(0); close(2); int fd2 = open("log2.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd2 < 0) return -1; printf("fd2 : %d\n",fd2); int fd3 = open("log3.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666 ); if(fd3 < 0) return -1; printf("fd3 : %d\n",fd3);return 0;
}
可以看到,我们关闭
0
、2
文件之后,新打开文件的文件描述符是0
和2
所以,这里我们文件描述符分配时,会从0
开始去找到一个最小且没有被使用的文件描述符,再进行分配。
操作系统对于打开的文件的管理
对于上面的文件描述符,可以说很抽象,为什么可以使用一个整数来表示一个打开的文件呢?
现在来看操作系统是如何对被打开的文件进行管理的。
要想进行管理,那一定是先描述,再组织
**先描述:**操作系统要像对被打开文件进行管理,那就要先描述这些被打开文件;在Linux
操作系统中就存在struct file
结构体来描述这些被打开的文件(struct file
里就可能有文件描述符等等一些关于文件的信息)。
所以,在打开一个文件时,就只需根据文件的相关属性构建相对应的
struct file
即可(还存在文件缓冲区,也会将部分内容拷贝到文件缓冲区中)
再组织: 操作系统要将这些被打开文件(struct file
)进行管理;在Linux
操作系统中就存在一个全局链表,所以被打开文件的struct file
都在这个链表当中。
这样操作系统就将所有被打开的文件管理起来的,但是现在还有一个问题,程序打开文件也就是进程要打开文件,那进程是如何打开的呢?
这个问题就比较简单了,我们的进程不是有对应的task_struct
吗,那我们进程打开了哪些文件就肯定在tasj_struct
中存储着相对应的信息。
在
task_struct
中,存在着一个指针struct file_struct* files
,这个指针指向进程相对应的文件描述符表
;而在
文件描述符表
中,存在着一个数组struct file* fd_array
;而我们的文件描述符就和这个
fd_array
数组的下标一一对应。
那么,也就是说我们文件描述符表在的fd_array
数组,下标0
、1
…和我们文件描述符一一对应。
那么,在进程执行的过程中,只需要访问
task_struct
中的文件描述符表中的fd_array
数组,就可以知道我们进程打开了哪些文件。
了解了操作系统对于被打开文件的管理,现在再来看文件描述符
它就是进程文件描述符表中
fd_array
的数组下标;在新建文件时,操作系统就会遍历进程的描述符表中
fd_array
数组,找到一个最小且没有被使用的数组下标来作为新打开文件的文件描述符。
stdin/stdout/stderr
和文件描述符0/1/2
的区别
我们知道,在C
语言程序启动时,它会默认打开三个文件stdin/stdout/stderr
,这是语言层面的概念;
而文件描述符0/1/2
这是操作系统层面的概念。
在C语言中stdin/stdout/stderr
的类型都是FILE*
,我们只知道FILE*
是文件类型的指针,但是FILE
它是什么呢?
其他的我们不懂,但是这里我们可以肯定在FILE
中肯定存在文件对应的文件描述符;(因为C语言文件操作是封装了系统调用,而系统调用是使用文件描述符来访问文件的,所以在FILE
肯定存在文件所对应的文件描述符)
并且我们也可以确定,stdin
对应的文件描述符肯定是0
、stdout
对应的文件描述符肯定是1
、stderr
对应的文件描述符肯定是2
。
这里提出一个问题?
我们知道文件描述符的分配规则是找一个最小且没有被使用的文件描述符进行分配。
那如果我们关闭了
0/1/2
文件,再创建文件那0/1/2
还是标准输入、标准输出和标准错误吗?
答案是的,在语言层我们标准输入stdin
中的文件描述符就是0
、标准输出stdout
中的文件描述符就是1
、标准错误stderr
中的文件描述符就是2
。
我们关闭
0/1/2
文件,这一操作是系统层的操作,我们语言层C
它并不知道;那也就是说,在程序往标准输出中输出数据时,它只会拿着
stdout
中的文件描述符1
去调用系统调用;那也就是说在系统层:文件描述符
0
指向哪个文件,哪个文件就是标准输入;文件描述符1
指向哪个文件,哪个文件就是标准输出;文件描述符2
指向哪个文件,哪个文件就是标准错误。
本篇文章到这里就结束了,感谢各位的支持。
继续加油!!!