目录
一、重谈文件
文件的基本知识:
- 文件由文件内容和文件属性两部分构成
- 空文件只是文件内容为空,但文件依旧存在属性,所以空文件也要占用磁盘空间
- 因为文件由内容和属性构成,所以对文件的操作 = 对内容操作 + 对属性操作 or 对内容和属性操作
- 标识一个文件:文件路径 + 文件名(唯一性)
- 如果没有指明路径,默认在当前路径下进行文件访问,当前路径是指当前进程的工作路径
- 在C语言中,对包含 fopen、fclose、fwrite、fread 等函数接口的程序进行编译链接形成可执行程序之后,如果不运行该可执行程序,相应的文件接口就没有被调用,则对应的文件操作也不会被执行。所以对文件的操作只有进程运行了才会对文件进行操作,运行起来的程序就是一个进程,所以对文件的操作本质上就是进程对文件的操作
- 一个文件没有打开,不能直接对文件进行访问;一个文件要被访问,就必须先被打开。怎么打开?用户进程 + OS(操作系统),进程负责调用文件接口,OS 负责打开相应的文件
- 一个文件被打开,并不是磁盘上所有的这些文件都被打开了,只是打开了相应的文件。所以磁盘上的文件分为 被打开的文件 + 未打开的文件。所以,文件操作的本质:进程 和 被打开的文件 的关系
- 而 没有被打开的文件 与文件系统相关,文件系统也是 基础IO 重点学的对象
二、C语言文件操作
2.1 重谈C语言文件操作
在学习文件操作之前,我们先来回顾一下C语言的文件操作
C语言文件操作部分接口
C语言文件操作函数 |
功能 |
fopen |
打开文件 |
fclose |
关闭文件 |
fputc |
写入一个字符 |
fgetc |
读取一个字符 |
fputs |
写入一个字符串 |
C语言文件操作函数 |
功能 |
fopen |
打开文件 |
fclose |
关闭文件 |
fputc |
写入一个字符 |
fgetc |
读取一个字符 |
fputs |
写入一个字符串 |
C语言文件操作函数 |
功能 |
fopen |
打开文件 |
fclose |
关闭文件 |
fputc |
写入一个字符 |
fgetc |
读取一个字符 |
fputs |
写入一个字符串 |
C语言文件打开的几种方式(部分)
文件打开方式
含义
如果指定文件不存在
“r”(只读)
为了输入数据,打开一个已经存在的文本文件
出错
“w”(只写)
为了输出数据,打开一个文本文件
建立一个新的文件
“a”(追加)
向文本文件尾添加数据
建立一个新的文件
“r+”(读写)
为了读和写,打开一个文本文件
出错
“w+”(读写)
为了读和写,建议一个新的文件
建立一个新的文件
“a+”(读写)
打开一个文件,在文件尾进行读写
建立一个新的文件
“rb”(只读)
为了输入数据,打开一个二进制文件
出错
“wb”(只写)
为了输出数据,打开一个二进制文
建立一个新的文件
“ab”(追加)
向一个二进制文件尾添加数据
出错
C语言文件操作的例子在 C语言专栏的文件操作里面,这里不再演示
默认打开的三个流,任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是 stdin、stdout 以及 stderr
随着我们的不断学习,接触的语言逐渐增多,比如面向过程语言C、面向对象语言 C++/java、静态编译语言 go、解释型语言 python 等等语言,它们的都有文件操作,并且接口都不一样,学习成本高。
但是它们底层调用的接口都一样。因为文件是在磁盘上,磁盘归 OS 管理,而所有人想访问磁盘都绕不开操作系统,想访问磁盘就必须使用 OS 提供的接口,所以想要访问文件,OS 就必须提供相关文件级别的系统接口,所以想访问文件就必须直接或间接使用操作系统提供的接口
所以无论上层的语言(C/C++/Java..)怎么变化,库函数的底层必须调用系统接口。库函数可以千变万化,但是底层是不变的,要降低学习成本,我们只要学习不变的东西即可!!
所以我们学习文件操作只需要学习操作系统系统调用有关文件操作方面的接口即可,以后我们再学习语言的文件操作时只需要学习一些新的特性即可,总体层面上是不变的(因为底层接口都一致),大大降低了学习成本
2.2 补充细节
测试代码
intmain() { FILE*fp=fopen(FILE_NAME, "w"); if(fp==NULL) { perror("fopen"); return1; } intcnt=5; while(cnt) { fprintf(fp, "%s, %d\n", "hello", cnt--); } fclose(fp); return0; }
运行结果
log.txt 文件创建出来的权限为 664,普通文件的默认权限为0666,Linux 下普通用户默认的 umask 为 0002,而文件最终权限等于默认权限 & ~umask,所以 log.txt 的权限为 0664
C语言以 “w” 方式打开文件时,无论是否写入新数据,都会清空之前文件中的数据
三、操作系统文件操作(系统文件I/O)
3.1 文件相关系统调用:close
close:关闭一个文件
man 2 close 进行查看
3.2 文件相关系统调用:open
open:打开或者创建一个文件
man 2 open 进行查看、
intopen(constchar*pathname, intflags); intopen(constchar*pathname, intflags, mode_tmode); pathname: 要打开或创建的目标文件flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags参数: O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND: 追加写mode: 指定创建新文件时文件的默认权限(文件最终权限还要受umask的影响) 返回值:成功:新打开的文件描述符失败:-1
3.2.1 open 的第二个参数 flags
open函数的第二个参数是 flags,表示打开文件的方式
系统接口 open的第二个参数flags是整型,有 32比特位,若将一个比特位作为一个标志位,则理论上 flags 可以传递32种不同的标志位。实际上传入 flags 的每一个选项在系统当中都是以宏的方式进行定义的
参数选项:
上述这些宏表示不同的文件打开方式,其原理是 通过比特位传递选项
什么是比特位传递选项?
//每一个宏只占用一个比特位,该比特位为1说明该选项成立,且各个宏的位置不重叠 voidshow(intflags) { //flags与上面哪个选项匹配,就执行对应的操作 if(flags&ONE) printf("one\n"); if(flags&TWO) printf("two\n"); if(flags&THREE) printf("three\n"); if(flags&FOUR) printf("four\n"); } intmain() { //主函数中通过传递不同选项来达到不同效果 show(ONE); printf("--------------\n"); show(ONE|TWO); printf("--------------\n"); show(ONE|TWO|THREE); printf("--------------\n"); show(ONE|TWO|THREE|FOUR); return0; }
运行结果
下面进行测试使用
intmain() { intfp=open(FILE_NAME, O_WRONLY);//以写的形式打开 if(fp<0) { perror("open"); return1; } close(fp); return0; }
运行结果
文件没有创建成功是因为 没有传创建参数 O_CREAT,系统接口默认是不会自动创建的,必须传创建参数
修改一下代码
运行结果,文件创建成功了
但是,这个文件有点不对劲,它的权限是乱码?这就跟第三个参数有关了
3.2.2 open 的第三个参数 mode
open函数的第三个参数是mode,表示创建文件的默认权限
但实际上创建出来文件的权限值还会受到 umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值普通用户为 0002,当我们设置 mode 值为 0666 时实际创建出来文件的权限为 0664
再次修改代码
运行结果,文件就可以正常创建了
3.2.3 open 的返回值
open函数的返回值是新打开文件的 文件描述符
文件描述符下面讲解
3.3 文件相关系统调用:write
write 系统接口的作用是向文件写入信息
man 2 write 查看
ssize_t write(int fd, const void *buf, size_t count);
使用 write 系统调用,将 buf 位置开始向后 count 字节的数据写入文件描述符为 fd 的文件当中
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回
测试代码
intmain() { intfp=open(FILE_NAME, O_WRONLY|O_CREAT, 0666);//以写的形式打开,不存在创建,权限设置为 0666 if(fp<0) { perror("open"); return1; } intcnt=5; charoutBuffer[64]; while(cnt) { sprintf(outBuffer, "%s:%d\n", "hello", cnt--); write(fp, outBuffer, strlen(outBuffer));//strlen 之后不用+1,系统接口是以有效数据结尾,不是C语言,C语言 strlen+1代表添加'\0',因为C语言的字符串是以'\0'结尾 } close(fp); return0; }
运行结果
修改一下写入文件的内容
再次运行,cat 查看文件内容
文件里面怎么有上一次的内容?怎么没有清理干净??
这是因为我们没有传清空文件内容的参数 O_TRUNC,修改代码
再次运行,cat 查看,上一次的内容就被清空了
C语言的 ‘w’ 则是自动清空上一次文件的数据,当你C语言在以 ‘w’ 的方式写入时,实际上操作系统在底层给你传了 O_WRONLY|O_CREAT|O_TRUNC, 0666,这就是系统接口与库函数的区别
3.4 文件相关系统调用:read
read 系统接口的作用是从文件读取信息
man 2 read 进行查看
ssize_t read(int fd, void *buf, size_t count);
使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中
- 如果数据读取成功,实际读取数据的字节个数被返回
- 如果数据读取失败,-1被返回
测试代码
intmain() { intfp=open(FILE_NAME, O_RDWR);//O_RDWR读 if(fp<0) { perror("open"); return1; } charbuffer[512]; ssize_tnum=read(fp, buffer, sizeof(buffer)-1); if(num>0) { buffer[num] =0;//C语言的字符串以'\0'结尾 } printf("%s", buffer); close(fp); return0; }
运行结果
由于C语言字符串以 ‘\0’ 结尾,而文件中的字符串数据并不包含 ‘\0’,所以这里我们需要预留一个位置,便于在数据量大于等于512字节情况下 buf 中仍有空间来放置 ‘\0’
总结
C语言的库函数接口 fwrite fread fclose fopen ..,类比系统调用接口 write read close open ...,实际上就是 C语言对系统调用接口进行封装形成库函数
四、文件描述符
4.1 如何理解文件
上面已经说过
文件操作本质上是 进程 与 被打开文件 之间的关系
一个进程可以打开多个文件,且操作系统同时运行着许多个进程,那么操作系统中就一定存在着大量被打开的文件
那这些被打开的文件要不要被操作系统管理起来呢?肯定要的
怎么管理?
先描述,在组织
操作系统为了管理被打开的文件,必定要为文件创建对应的 内核数据结构用于标识文件,在 Linux 中,这个数据结构是 struct file {},这个结构体包含了文件的大部分属性(这个结构体与C语言的 FILE 没有任何关系)
4.2 什么是文件描述符
open 系统调用的返回值是新打开文件的 文件描述符
测试代码
C语言 # 在宏当中的作用 – 将参数插入到字符串中
intmain() { intfd0=open(FILE_NAME(1), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd1=open(FILE_NAME(2), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd2=open(FILE_NAME(3), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd3=open(FILE_NAME(4), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd4=open(FILE_NAME(5), O_WRONLY|O_CREAT|O_TRUNC, 0666); printf("fd0:%d\n", fd0); printf("fd1:%d\n", fd1); printf("fd2:%d\n", fd2); printf("fd3:%d\n", fd3); printf("fd4:%d\n", fd4); close(fd0); close(fd1); close(fd2); close(fd3); close(fd4); return0; }
运行结果
从运行结果可以看到,文件描述符是连续分配且依次增大的,这也很合理,因为文件描述符本质上是数组下标,而连续增长正好是数组下标的特性;但是这里有一个很奇怪的地方 – 文件描述符是从3开始的,但是0、1、2 呢?
这是由三个默认打开的标准流引起的:标准输入stdin、标准输出流stdout 与 标准错误流stderr
man stdin 查看,这三个流都是 FILE* 的指针,FILE 结构体里面就一定会封装一个变量来表示文件描述,这个这个变量是 _fileno
在C语言中,打开一个文件:FILE* fp = open(),这个 FILE 实际上也是一个结构体,它底层封装采用的是系统调用,所以必定有一个字段叫做:文件描述符
所以,我们进行打印一下这三个值
intmain() { printf("stdin->fd:%d\n", stdin->_fileno); printf("stdout->fd:%d\n", stdout->_fileno); printf("stderr->fd:%d\n", stderr->_fileno); intfd0=open(FILE_NAME(1), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd1=open(FILE_NAME(2), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd2=open(FILE_NAME(3), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd3=open(FILE_NAME(4), O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd4=open(FILE_NAME(5), O_WRONLY|O_CREAT|O_TRUNC, 0666); printf("fd0:%d\n", fd0); printf("fd1:%d\n", fd1); printf("fd2:%d\n", fd2); printf("fd3:%d\n", fd3); printf("fd4:%d\n", fd4); close(fd0); close(fd1); close(fd2); close(fd3); close(fd4); return0; }
运行结果
从运行结果就可以知道, 因为三个标准流是默认打开的,所以 0、1、2 是默认被占用的,所以文件描述符默认是从 3 开始的
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象
而进程执行 open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
4.3 文件描述符的分配规则
测试代码
intmain() { intfd=open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666); if(fd<0) { perror("open"); return1; } printf("open fd: %d\n", fd); close(fd); return0; }
运行结果,是3,没毛病,0、1、2 默认被占用
若我们先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?
修改一下代码
再次运行,可以看到,打开的文件获取到的文件描述符变成了0
再次修改代码
intmain() { close(0);//关闭文件描述符0 close(2); intfd1=open("log.txt1", O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd2=open("log.txt2", O_WRONLY|O_CREAT|O_TRUNC, 0666); intfd3=open("log.txt3", O_WRONLY|O_CREAT|O_TRUNC, 0666); printf("open fd1: %d\n", fd1); printf("open fd2: %d\n", fd2); printf("open fd3: %d\n", fd3); close(fd1); close(fd2); close(fd3); return0; }
运行结果
可以看到,当 0 和 2 文件描述符被关闭以后,系统将其分配给了新打开的文件
close 关闭文件并不是将 fd 指向的文件对象释放掉,而仅仅是让当前进程文件描述符表中的对应下标不再指向该文件对象,因为同一个文件可能会被多个进程访问
其底层采用 count “引用计数” 的方式来实现,即当有指向该文件的进程关闭时文件计数减1,有指向该文件的进程打开时文件计数加1,当 ount 为 0 时操作系统才释放该文件的内核数据结构,即真正意义上的闭文件
所以,文件描述符的分配规则是:在 files_struct 数组当中,从小到大依次搜寻,找到当前没有被使用的最小的一个下标,作为新的文件描述符
----------------我是分割线---------------
文章到这里就结束了,下一篇即将更新