📌 个人主页: 孙同学_
🔧 文章专栏: Liunx
💡 关注我,分享经验,助你少走弯路!

文章目录
-
- [一. 理解文件](#一. 理解文件)
-
- [1.1 侠义理解](#1.1 侠义理解)
- [1.2 广义理解](#1.2 广义理解)
- [1.3 文件操作的归类认知](#1.3 文件操作的归类认知)
- [1.4 系统角度](#1.4 系统角度)
- [二. 回顾C语言文件接口](#二. 回顾C语言文件接口)
-
- [2.1 打开文件](#2.1 打开文件)
- [2.2 对文件进行写入](#2.2 对文件进行写入)
- [2.3 输出信息到显示器,有哪些方法](#2.3 输出信息到显示器,有哪些方法)
- [2.4 stdin & stdout & stderr](#2.4 stdin & stdout & stderr)
- [2.5 打开文件的方式](#2.5 打开文件的方式)
- [三. 系统文件I/O](#三. 系统文件I/O)
-
- [3.1 位图传递标志位](#3.1 位图传递标志位)
- [3.2 open](#3.2 open)
- [3.3 write](#3.3 write)
- [3.4 read](#3.4 read)
- [四. 访问文件的本质](#四. 访问文件的本质)
-
- [4.1 文件描述符](#4.1 文件描述符)
一. 理解文件
我们以前就说过 文件 = 文件内容 + 文件属性
1.1 侠义理解
- 文件是在磁盘上的,磁盘本质上是个外设,我们访问文件其实就是在系统和磁盘上进行IO
- 磁盘是永久性的存储介质,文件在磁盘上的存储是永久性的
- 对文件的所有操作,本质上是对外设的输入输出 简称 IO
1.2 广义理解
Linux
下一切皆文件(键盘,显示器,网卡,磁盘... )
1.3 文件操作的归类认知
- 对于0KB的空文件,是占据磁盘的空间的
- 文件 = 文件内容 + 文件属性
- 所有对文件的操作本质上是对文件内容和文件属性的操作
1.4 系统角度
- 对文件的操作本质上是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读或写本质上不是通过
C语言/C++
的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口实现的
文件
- "内存级(被打开)"文件
- 磁盘级文件
二. 回顾C语言文件接口
2.1 打开文件
cpp
// 文件打开接口
FILE *fopen(const char *path, const char *mode);
path
:表示要打开文件的路径,或者文件名,只有文件名而没有路径表示打开当前路径下的文件。mode
:表示打开的方式,比如只读r
,只写w
,追加a
等。
📌 Tips: 我们之前介绍的重定向,>
本质上就对应使用的是w
选项,>>
本质上就对应使用的是a
选项。
2.2 对文件进行写入
cpp
#include<stdio.h>
int main()
{
FILE* fp = fopen("myfile.txt","w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
while (1);
fclose(fp);
return 0;
}
打开的myfile
文件在哪个路径下呢?
- 在程序的当前路径下
- 那系统怎么知道程序的当前路径在哪里呢?
可以使用ls /proc/[进程id] -l
命令查看当前正在运行进程的信息:

cwd
:进程当前的工作路径exe
:指向启动当前进程的可执行文件(完整路径)的符号链接。
2.3 输出信息到显示器,有哪些方法
printf()
fprintf()
fwrite()
当我们向显示器打印本质上就是向显示器文件写入,Linux
下一切皆文件
2.4 stdin & stdout & stderr
- c语言会默认打开三个输入输出流,分别是
stdin
,stdout
,stderr
- 这三个流的类型都是
FILE*
为什么要帮我们把这几个流自动打开呢?
我们传统上写的程序是做数据处理的
2.5 打开文件的方式
- 以
w
的方式打开文件时,文件首先会被清空,然后从0开始写
- 我们以前说过的重定向,比如
echo aaaaa > log.txt
,把打印到显示器上的内容写入文件里,前提是我们先得把文件打开。我们的输出重定向>log.txt
为什么会把文件内容清空呢?因为我们输出重定向第一步要打开文件,而打开文件,而打开文件第一步先要把文件清空
- 以
a
的方式打开文件,这种方式叫做追加 ,它一般写的时候会向文件结尾进行写入,不存在的话就创建它。>>
叫做追加重定向,以a
的方式进行写入本质上也是先要把文件打开,然后再进行写入。
- 当我们向文件里写入一段字符串时,我们需不需要在字符串后面加
\0
呢?答案是不需要,因为\0
是c语言的规定,与我文件又有什么关系呢。
三. 系统文件I/O
我们对文件操作的是时候,文件是在磁盘上面的,而真正对文件进行操作的其实是操作系统,操作系统对磁盘文件进行读写访问,我们以前使用c语言对文件的访问其实是c语言封装了系统调用。比如说访问文件得先打开,那么就得先有open
flags
有众多选项O_RDONLY
表示只读, O_WRONLY
表示只写, O_RDWR
表示读写,O_TRUNC
表示
cpp
int open(const char *pathname,int flags,mode_t mode);
如果我们今天要打开一个文件,并且这个文件不存在要新建的话,就用上面的这个open
,必须指定权限,如果不指定的话这个权限就是乱码。如果打开一个已经存在的文件,就用两个参数的。
open
的返回值:如果打开成功的话返回一个新的文件描述符,如果失败的话-1
被返回,并且错误码就被设置了。
flags
是一种整形标志位,一共有32个bit
位,,如果用O_RDONLY
这种选项直接传参的话会很麻烦,所以选用位图的方式来传递。
3.1 位图传递标志位
cpp
#include<stdio.h>
#define ONE_FLAG (1<<0)// 000000....00000001
#define TWO_FLAG (1<<1)// 000000....00000010
#define THREE_FLAG (1<<2)// 000000....00000100
#define FOUR_FLAG (1<<3)// 000000....00001000
void Print(int flags)
{
if(flags & ONE_FLAG)
{
printf("One!\n");
}
if(flags & TWO_FLAG)
{
printf("Two!\n");
}
if(flags & THREE_FLAG)
{
printf("Three!\n");
}
if(flags & FOUR_FLAG)
{
printf("Four!\n");
}
}
int main()
{
Print(ONE_FLAG); //打印one
printf("\n");
Print(ONE_FLAG | TWO_FLAG); //打印one two
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);//打印one two three
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);//打印one two three four
printf("\n");
return 0;
}
3.2 open
我们open
打开文件的时候绝对相对路径都可以,因为在哪个路径下创建文件是由进程决定的,进程记录了自己的cwd
,说明我们新建文件是在指定路径下建还是在其他路径下建和cd
,fopen
都没有关系,它是系统的行为。
我们接下来验证一下log.txt
它默认会在当前路径下去创建,因为它进程的路径就在当前路径下。
我们对文件进行写入write
接口
cpp
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- 第一个参数: 是
open
的返回值,这个返回值叫文件描述符 - 第二个参数: 是我们要写入的
buffer
- 第三个参数: 是我们要写的数据的长度,返回值是实际写入的长度。写入失败返回
-1
。
cpp
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);//在新建文件之前将umask权限眼掩码设置为0
int fd = open("log.txt",O_CREAT | O_WRONLY,0666);//不存在就创建,而且以写入的方式打开
if(fd < 0)
{
perror("open");
return 1;//进程的退出码设为1
}
printf("fd = %d/n",fd);
const char *msg = "hello world\n";//定义一个字符串
int cnt = 5;
while(cnt)
{
write(fd,msg,strlen(msg));//向指定文件描述符里写,写的内容是msg,写的长度
//我们在写入时是当做字符来写,所以这里不需要strlen(msg)+1,因为\0是c语言规定的,如果写\0的话在我们的文件中就会出现@^乱码现象
cnt--;
}
close(fd);//关闭文件
return 0;
}

当我们把要写的内容该为abcd
并且只让它写一行。
现象是在我们往文件里写的时候应当是先清空再进行写入 ,而现在是覆盖写 ,文件内原来的内容并没有清空,为什么没有清空呢?答案是我们再创建文件的时候,只填了创建 和写入 ,而并没有要清空 。
所以我们加上O_TRUNC
💦结论:如果我们要打开文件,并且将它清空,若要用系统级的函数,我们就需要传递这几个标志位。
清空并写入:
cpp
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
如果O_CREAT
(不存在就新建),O_WRONLY
(以只写入的方式),O_TRUNC
(清空)
追加并写入:
cpp
int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND,0666);
所以C语言中的fopen
中的w
选项和a
选项就会被分别转化为上面的那样。
3.3 write
write
这个接口在写的时候参数不是char*
,而是void*
cpp
ssize_t write(int fd, const void *buf, size_t count);
说明write
在进行写入的时候,进行二进制写入也可以,做字符串写入也可以。
文本写入 vs 二进制写入
我们在往显示器上打印12345
的时候是往显示器打印的是1
字符2
字符3
字符4
字符和5
字符。而不是一万两千三百四十五。
📌小tips: 我们往显示器上打印和我们往文件里写入其实是一摸一样的,因为Linux
下一切皆文件。
我们往文件里以清空写的方式写入1234567
查看log.txt
我们会发现log.txt
是4
个字节里面的内容是乱码,这是为什么呢?因为这种写入叫做二进制写入,在实际写的时候把a
这个整形变量写到了文件里面,所以文件的大小是4
字节,因为整数的大小是4
字节,而我们写入的1234567
不可显,它是把1234567
这个二进制数字写在了磁盘上。而我们想看到的是1234567
,那可怎么办呢?
将a
这个整形格式化处理成字符串,然后将字符串写入到要写的文件中。
此时的log.txt
中就是1234567
了。
💦结论 : 在系统层面上并不存在所谓的文本写入和二进制写入,系统并不关心你写入的类型,文本写入还是二进制写入其实是语言层 提供的概念。所以我们在c语言中有fpus
文本写入fwrie
二进制写入,这两个底层最终调用的都是这个接口。
cpp
ssize_t write(int fd, const void *buf, size_t count);
格式化输入输出其实就是将内存里的二进制数据转成字符串,在使用write
接口将数据写进去。所以格式化输入输出,文本式的写入全都是语言层的概念。这个格式化工作要么是语言来做,要么是我们自己来做。
3.4 read
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- 返回值 :如果成功,则返回读的字节数(0表示文件结束),如果错误返回
-1
- 第一个参数 :是
open
的返回值,这个返回值叫文件描述符 - 第二个参数: 指向一段空间,该空间用来存储读取到的内容。
我们读取log.txt
中的内容
四. 访问文件的本质
我们一次性打开四个文件,并观察它的fd

打出来的文件描述符是3
,4
,5
,6
问题是0
,1
,2
去哪里了呢?
0
:标准输入1
:标准输出2
:标准错误
这三个叫做默认的文件流,因为它默认的把三个文件打开了,012
已经被占了。
C语言
中有三个标准输入,标准输出,标准错误的文件流
C++
中也有标准输入(cin)
,标准输出cout
,标准错误cerr
的文件流
C语言
中我们打开文件叫做FILE*
,*
是指针,可是FILE
是什么呢?FILE
是C语言
提供的一个结构体,它是被typedef
出来的,结构体里包含了文件的属性。
在OS
接口层面上只认fd
文件描述符,所以这个结构体里一定封装了文件描述符 。
所以我们以前学到的文件操作,在类型层面的文件对象FILE
封装了文件描述符,在接口层面打开文件是封装了对应的选项。
所以任何语言,底层只认文件描述符,C语言
/C++
会把市面上的各种平台的代码各自实现一份,然后采用条件编译,代码裁剪的方式,把不同的系统需要的库编到不同的系统里,给用户提供的是同一个语言型 接口,这样写出来的代码具有可移植性 。
要了解可移植性就需要知道不可移植性,不可移植性是由于平台不一样,平台不一样系统调用的接口不一样。
4.1 文件描述符
操作系统要把被打开的文件管理起来,怎么管理呢?先描述,再组织
创建一个进程的时候,首先在内核当中创建的了一个task_struct
,我们称之为进程控制块。操作系统在打开文件时需要在内核当中创建一个数据结构struct file
,打开很多文件就会创建很多的struct file
,然后用指针连接起来。那么找一个文件的所有内容或者任何一个属性就都能通过struct file
找到。也就是说未来想访问文件就只需要找到对应的struct file
结构体队对象就可以了。
在文件中,每一个文件的struct file
都会提供一个文件级缓冲区,将来文件的内容就会加载到文件缓冲区当中,文件的属性会用来初始化我们的struct file
以及将来的inode
结构体。今天我们如果是想读取文件里面的内容,一定先是我们把文件打开,创建struct file
结构体,通过file
内部的指针操作找到文件缓冲区,然后操作系统把文件的内容给我们加载或者预加载到缓冲区里面,加载之后我们读写文件的本质就是从缓冲区里面把内容拷贝出去。
可是在进程中怎么快速找到我们自己打开的文件呢?被连起来的文件有可能属于进程a
,也有可能属于进程b
,哪个文件是和你的进程相关的呢?在我们的进程的PCB
当中,当一个进程被创建,除了地址空间页表,它还要创建一个struct files_struct
文件描述符表 ,文件描述符表中包含一个数组,这个数组是可大可小的,一般的Linux
系统是32
或者64
,在云服务器上它可以支持内核扩展,这个struct files_struct
中还包含了其他属性,包括打开文件的个数,其他的一些属性信息。这个数组叫做struct file *fd array[]
,这是一个指针数组,在PCB
中会存在一个struct files_struct *files
指向文件描述符表。这个数组中放的就是这个进程打开的文件,把操作系统打开的struct file
对象填充到我们数组对应的指定下标当中。此时那些进程有哪些文件就被关联起来了,建立被打开的文件和进程之间的映射关系 。所以进程要访问任意一个被打开的文件就可以通过下标来访问了。
所以文件描述符的本质就是数组下标
当我们的用户层在进行open
调用的时候,就会在操作系统里创建一个新的struct file
,然后在当前进程的文件描述符表里面找到一个没有被使用的下标,然后把struct file
的地址填进去,此时进程和文件就关联了。下来读取数据(read)时要传fd
,当前进程调用read
,进程拿着fd
去文件描述符中的数组中找,就能找到对应的文件,每一个文件都有对应的文件缓冲区,操作系统把磁盘中的内容预加载到缓冲区当中,然后把文件缓冲区当中的数据拷贝到用户层的buffer
中。
文件描述符的分配原则:最小的没有被使用的作为新的fd
分配给用户。
👍 如果对你有帮助,欢迎:
- 点赞 ⭐️
- 收藏 📌
- 关注 🔔