目录
1.回顾c语言中所学的文件
2.提炼对文件的理解(linux基础io第一阶段的学习)
a.在操作系统内部,一个进程和一个被打开的文件,他们到后面会变成两种对象之间的指针关系。
b.文件 = 属性 + 内容
c.在c语言中,以w的方式打开文件,默认打开文件的时候,就会先把目标文件给清空。a则是追加
d.输出重定向 ‘ > ’--->先清空再写入(以w方式打开文件),追加重定向 '>>'具体可以看博客linux-基本指令与用法02(超详细!!)_linux的02-CSDN博客
3.理解文件
open()系统调用函数
参数说明
返回值
打开现有文件(只读)
独占创建文件(避免竞态条件)
创建新文件(读写权限,若存在则清空)
设计传递位图标记位的函数
close()系统调用函数
close() 的作用
close()参数:
write()系统调用函数
参数说明:
返回值:
write() 与 fwrite() 对比
什么叫做fd(文件描述符):
为什么不返回 0、1、2?
fd的本质是什么?
fd的本质是:内核的进程:文件映射关系的数组的下标
一切皆文件
c语言上的文件操作函数,本质底层都是对系统调用的封装
c语言为什么要这么做:
1.回顾c语言中所学的文件
进行文件操作,前提是我们的程序在执行了,所谓的文件的打开和关闭,是cpu在执行代码,比如执行fopen(),才将文件打开的。
当一个代码执行的时候已经变成了一个进程,所以在建立文件时,他就会默认的接在当前进程所处路径后,拼上所创建的文件名,创建的这个文件。
(进程启动时所处的路径:当前进程的当前工作路径。)
2.提炼对文件的理解(linux基础io第一阶段的学习)
a.在操作系统内部,一个进程和一个被打开的文件,他们到后面会变成两种对象之间的指针关系。
- 打开文件,本质是进程打开文件。
- 当文件本就存在时,文件没有被打开的时候,存在磁盘上
- 一个进程可以打开多个文件。
- 系统当中可以存在多个进程,大多数情况下,OS内部,一定存在大量被打开的文件,每个进程可能都打开很多文件。
- 文件未被打开时处于磁盘上,处于一个硬件上,因此只能被操作系统(操作系统是硬件的管理者)打开(用的是c语言接口),因此操作系统会对这些被打开的文件进行管理(先描述,再组织)。
- 每一个被打开的文件,在os内部,一定会存在对应的描述文件属性的结构体,类似于PCB。
b.文件 = 属性 + 内容
当我在磁盘上新建一个文本文件,但并不打开,并不往其中填写任何数据,他在磁盘上所占据的大小是0KB,此时他是否会占据磁盘空间?会占据,文件的名字、文件建立的时间、文件的大小等等文件的属性就已经占据了磁盘空间了。0kb指的是内容为0。结构体放的就是文件的属性。
1.打开文件 2.并向文件 写入3.再关闭。
1.打开文件 --- w ,不存在就在当前路径下创建指定文件。 2.并向文件 写入3.再关闭。都是进程让cpu在执行自己的代码。通过这样的方式发开文件,访问文件。
c.在c语言中,以w的方式打开文件,默认打开文件的时候,就会先把目标文件给清空。a则是追加
只以写的方式打开文件:
延续上面的操作本来log.txt中是有内容的,现在被清空了
d.输出重定向 ‘ > ’--->先清空再写入(以w方式打开文件),追加重定向 '>>'具体可以看博客linux-基本指令与用法02(超详细!!)_linux的02-CSDN博客
3.理解文件
a,操作文件、本质:进程在操作文件。进程和文件的关系
b.文件 --> 磁盘(外设)---> 硬件 ----> 向文件中写入,本质是向硬件中写入。 ----> 用户没有权利直接向硬件写入 ----> 硬件的管理者是操作系统 ------> 用户无法绕过操作系统去处理硬件(嵌入式除外)----> 用户必须通过OS来写入 ----> 操作系统给用户提供系统调用 ----> c语言 / c++ ...都是对系统调用接口的封装 ----> 访问文件,就可以用系统调用
c++写入文件:
#include<iostream>
#include<string>
#include<fstream>#define FILENAME "log.txt"using namespace std;int main()
{std::ofstream out(FILENAME, std::ios::binary);if(!out.is_open()) return 1;string message("hello c++\n");out.write(message.c_str(), message.size());out.close();return 0;
}
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);
参数说明
pathname
类型:
const char*
作用:要打开或创建的文件路径(绝对或相对路径)。
flags
类型:
int
作用:指定文件的打开方式,多个标志可通过按位或(
|
)组合。常用标志:
必选标志(三选一):
O_RDONLY
: 只读模式、O_WRONLY
: 只写模式、O_RDWR
: 读写模式。可选标志:(O代表open的意思)
O_CREAT
:文件不存在时创建新文件,需配合
mode
参数设置权限。
O_TRUNC
:若文件存在且为普通文件,将其长度截断为0。
O_APPEND
:追加写入(每次写操作前移动到文件末尾)。
O_EXCL
:与
O_CREAT
联用时,若文件已存在则返回错误(用于原子性创建文件)。
O_NONBLOCK
:非阻塞模式(对设备文件或管道有效)。
mode
类型:
mode_t
作用:创建文件时的权限(仅当使用
O_CREAT
时需指定)。常见值(八进制表示):
0644
: 用户可读写,组和其他用户只读。
0755
: 用户可读写执行,组和其他用户可读执行。注意:实际权限为
mode & ~umask
(umask
用于过滤权限位)。返回值
成功:返回文件描述符(非负整数),用于后续操作(如
read
,write
)。失败:返回
-1
,并设置errno
指示错误类型(如ENOENT
文件不存在、EACCES
权限不足)
打开现有文件(只读)
当已经存在文件时,可以不传权限的参数
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>int main() {int fd = open("test.txt", O_RDONLY);if (fd == -1) {perror("open failed");return 1;}close(fd);return 0;
}
独占创建文件(避免竞态条件)
int fd = open("unique.txt", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd == -1) {perror("file already exists");return 1;
}
创建新文件(读写权限,若存在则清空)
创建新文件时,一定要传权限码,例如:0666:用户、组、其他,默认都是可读可写的权限
-rwxr-xr-- 1 user group 4096 Jun 1 10:00 file.txt
▲ ▲▲▲ ▲▲▲ ▲▲▲
│ │││ │││ │││
│ └──────┬─────── 权限(用户u、组g、其他o)
└───────── 文件类型(`-`普通文件,`d`目录)
八进制:
数字表示(八进制):
r=4, w=2, x=1,三者相加:
rwxr-xr--
→7(4+2+1) 5(4+0+1) 4(4+0+0)
→ 权限数字 754
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); if (fd == -1) {perror("open");return 1; }
没有传权限码就会出现权限处乱码:
传了0666:
这是因为:最终权限 = 默认最大权限 - umask 值(实际是位运算
默认权限 & ~umask
)
如何一次性创建好需求权限的文件:
umask()系统调用
#include <sys/types.h>#include <sys/stat.h>mode_t umask(mode_t mask);
修改原代码:将系统掩码设置为0,没设置的时候就用系统默认的
umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd < 0){perror("open");return 1;}
输出:
补充:文件权限计算:
默认最大权限:
666
(二进制110 110 110
)umask 值:
002
(二进制000 000 010
)实际权限:
666 - 002 = 664
(即
rw-rw-r--
,用户和组可读写,其他只读)将这个整数参数看作是一张位图
设计传递位图标记位的函数
通过设计一个传递位图标记位的函数来理解os 设计很多系统调用接口的方法:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define ONE 1 // 1 0000 0001 #define TWO (1<<1) // 2 0000 0010 #define THREE (1<<2) // 4 0000 0100 #define FOUR (1<<3) // 8 0000 1000void print(int flag) {if(flag & ONE)printf("one\n");if(flag & TWO)printf("two\n");if(flag & THREE)printf("three\n");if(flag & FOUR)printf("four\n"); }int main() {print(ONE);printf("\n");print(TWO);printf("\n");print(ONE | TWO);printf("\n");print(ONE | TWO | THREE);printf("\n");print(ONE | FOUR);printf("\n");print(ONE | TWO | THREE | FOUR);printf("\n");return 0; }
通过标志位来让我们实现对应的功能,向指定的函数传递多种标记位的方法,标记位传参
close()系统调用函数
close()
的作用释放资源:
- 每个打开的文件描述符都会占用系统资源(如内核中的文件表项、缓冲区等)。
- 调用
close()
后,系统会释放这些资源。刷新缓冲区:
- 如果文件是以写入模式打开的,
close()
会确保所有缓冲区的数据写入磁盘(类似于fflush()
)。解除文件描述符的绑定:
- 关闭后,文件描述符不再与任何文件或资源关联,可以被重新用于其他文件。
避免资源泄漏:
- 如果不关闭文件描述符,可能会导致文件描述符耗尽(每个进程有文件描述符数量限制)。
close()参数:
#include <unistd.h>int close(int fd);
fd
:要关闭的文件描述符(通常由open()
、socket()
等函数返回)。返回值:
成功时返回
0
。失败时返回
-1
,并设置errno
表示错误原因。
write()系统调用函数
一、函数原型与头文件
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd
(文件描述符)
- 已打开文件的描述符(由
open()
、socket()
等函数返回)。- 必须具有 可写权限(例如以
O_WRONLY
或O_RDWR
模式打开)。
buf
(数据缓冲区)
- 指向用户空间缓冲区的指针,包含待写入的数据。
- 可以是任意类型的数据(如字符串、二进制数据)。
count
(写入字节数)
- 指定从
buf
中写入的字节数。- 实际写入的字节数可能小于
count
(需检查返回值)。返回值:
成功时:返回实际写入的字节数(
0 ≤ 返回值 ≤ count
)。
- 返回值为
0
表示未写入数据(例如写入到已满的管道)。- 返回值小于
count
表示部分写入(需处理剩余数据)。失败时:
- 返回
-1
,并设置全局变量errno
表示错误类型
write()
与fwrite()
对比
特性 write()
fwrite()
接口层级 系统调用(底层) 标准库函数(高层) 缓冲 无缓冲(直接写入内核) 带用户空间缓冲区 错误处理 通过 errno
和返回值通过返回值与 ferror()
适用场景 需要精细控制的场景(如非阻塞) 常规文件操作(更便捷)
什么叫做fd(文件描述符):
strlen 函数:只用写入有效字符串
输出:
很显然第二次的写入是在上一次的基础上从头开始写的。
以写的方式打开,不存在就创建,并且先清空文件内容
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
open()的返回值fd是什么?这里我创建了四个文件并记录他们的返回值
输出结果:
为什么不返回 0、1、2?
因为:
- 1:标准输入 - 键盘
- 2:标准输出 - 显示器
- 3:标准错误 - 显示器
这里和c语言的进行对比
stdin、stdout、stderr对应的类型都是文件指针,与c语言的fopen、fdopen、freopen的返回值是一样的
这些说明,在c语言中我们把键盘显示器也是当做文件来看的
也就是说,由于write()是根据open返回的fd,来查找文件并写入的,那我直接往1中写入不就是往显示器文件中写吗:
编译运行:
fd ---> 文件描述符 ---> 文件描述符的本质是什么?
fd的本质是什么?
1.在打开文件的时候,会在操作系统中创建一个struct file,文件的内核数据结构所包含的是文件的属性(权限,什么方式被打开,标记位),所有被打开的文件,他们的内核数据结构以双链表的形式被链接起来,操作系统对文件的管理转变成对链表的增删查改,每一个struct file内部都有一个指向与 该文件所对应的文件内核级的缓存的 指针,操作系统给文件申请的内存。
一个磁盘上的文件,会经过属性struct file内核数据结构 初始化,内容直接存到这个文件的缓存当中。未来直接从缓存当中读写修改。
os中有多个进程,每个进程都有可能打开多个文件,进程和文件的关系是1:n,进程的内核数据结构中存在一个struct files_struct *file属性。os中还会存在一个struct file_struct内核数据结构,整个结构中会包含一个指针数组struct file* fd_array[N]。
想让进程和对应的文件产生关系:
将描述文件的结构体变量的地址(文件属性的地址),依次填入到 fd_array[],特定的数组中,因此一个进程想要找到对应的文件,只需要把对应文件数组的下标返回给上层比如说 int fd,就可以访问文件了。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
fd的本质是:内核的进程:文件映射关系的数组的下标
读的本质是:将文件缓冲区的内容拷贝到需要的文件当中去,如果对应的内容不在缓冲区里,os就会把这个打开文件的进程阻塞住,挂起,os再将磁盘中的数据搬到缓存当中,再唤醒进程。
写和修改内容:没有内容的时候上层就直接将内容拷贝到缓冲区,当文件中本来就有内容的时候,现将内容拷贝到缓冲区,再在内存当中修改,定期由os再刷新
1.无论读写,都必须让os把内容读到对应的文件缓冲区内,在内存中修改,再刷新到磁盘。
2.open()在:
a.创建file
b.开辟文件缓冲区的空间,加载文件数据(延后)
c.查进程的文件描述符表
d.将file地址,填入对应的表中
e.返回下标
3.write()、read()函数的本质是拷贝函数
一切皆文件
像是键盘、鼠标、显示器、网卡、磁盘这些外设,他们可以由一个设备结构体来记录他们的属性,但是,他们每一个的操作方法都不同,这是通过驱动来控制的。对每一个设备os都会构建一个struct file,里面就会包含他们的读写的函数指针,再指向驱动层的方法。使用同一个类,其中包含的读写函数指针指向不同的设备,因此我们就不用再管底层的差异了,因为底层外设的方法--->归于函数指针 ---》一切皆文件
这就像c++中的多态
这是一个指针指向一张操作表:
每一个被打开的文件还会有一张,操作底层方法的指针表
在操作系统中,这就叫做虚拟文件系统:virtual file system
在操作系统中,系统访问文件时只认文件描述符fd
如何理解c语言通过FILE* 访问文件? 这个FILE是一个结构体
因此这个FILE里面一定封装了fd文件描述符
c语言上的文件操作函数,本质底层都是对系统调用的封装
写如下代码:
FILE* fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}printf("fd: %d\n",fp->_fileno);fwrite("hello\n", 5, 1, fp);fclose(fp);
运行结果:这就证明了我们的FILE结构体中封装了fd文件描述符,
更进一步:
代码:
FILE* fp1 = fopen("log1.txt", "w");if(fp1 == NULL){perror("fopen");return 1;}printf("fd1: %d\n",fp1->_fileno);FILE* fp2 = fopen("log2.txt", "w");if(fp2 == NULL){perror("fopen");return 1;}printf("fd2: %d\n",fp2->_fileno);FILE* fp3 = fopen("log3.txt", "w");if(fp3 == NULL){perror("fopen");return 1;}printf("fd3: %d\n",fp3->_fileno);fclose(fp1);fclose(fp2);fclose(fp3);
输出结果:
c语言的stdout:stdin:stderr:
printf("stdin ->fd: %d\n", stdin->_fileno);printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd: %d\n", stderr->_fileno);
最后输出
c语言为什么要这么做:
本来可以使用系统调用,也可以使用语言提供的文件方法
系统不一样,系统调用接口就不一样,代码不具有跨平台性,而为什么c语言、c++....等所有的语言都具有跨平台性的原因和作用我们现在就知道了。
如图:
所有的语言要对不同的平台的系统调用进行封装,不同语言封装时,文件接口就有差别了
在c++中的cin、cout、cerr可以向文件,显示器都写,我们称他们为流,但cin、cout、cerr在c++中都叫做类,他们内部一定包含了文件描述符。
通过进程pid找到fd
终端文件:
这个终端也是属于一个文件,因此实际上我也可以向这个终端直接写东西:
运行结果:
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。