文章目录
文件
前置知识
在磁盘中的文件:
文件=内容+属性
访问文件之前,为什么文件必须打开?
-
文件什么时候才会被打开?
文件只有在代码编译形成可执行程序,可执行程序加载到内存变成进程,运行到打开文件的函数时文件才会被打开
-
访问文件的是谁?
是进程是进程打开文件,再使用函数封装的系统调用对文件进行访问的
-
文件在打开之前,在磁盘中
进程运行的本质是:
CPU执行进程的代码,访问进程的数据
进程对文件读取/写入时,本质是数据流转(拷贝),所以这个操作也是由CPU执行
但是根据冯诺依曼体系结构,CPU是不能直接访问磁盘的,因为这样太慢了
所以文件必须加载到内存之后,才能直接访问所以打开文件的本质是把文件加载到内存
文件的管理
一个进程可以打开多个文件,而被加载到内存的进程需要被管理
那被加载到内存的比进程个数更多的文件也一定会被操作系统管理起来
所以在内存中的文件:
文件=文件的内核数据结构+文件里面的内容
而磁盘中的文件也=内容+属性
所以:
内存中的文件的内容就是磁盘中文件的内容
内核数据结构中存储了文件的属性
标准流
一个进程在启动时,会默认打开3个流:
①标准输入,底层对应键盘
②标准输出,底层对应显示器
③标准错误,底层对应显示器
因为Linux下一切皆文件,键盘和显示器也被包装成了文件
所以我们可以使用文件指针(FILE*)的方式访问它们
标准错误流的作用是什么?
标准输出和标准错误都是把信息打印到显示器,那为什么还要有标准错误呢?
直接用标准输出打印错误不就行了吗?
这是因为人们使用标准输出时,可以打印各种各样的信息
而人们使用标准错误时,一般只会打印某些代码出错时的错误信息
而且标准输出和标准错误虽然都是向显示器写入,但是标准输出和标准错误它们两个对应的是不同的文件,文件描述符也不同
可以根据这个特点,虽然它们两个打印的信息都是打印到显示器中,但是我们可以把它们打印到信息分离保存到不同文件中
,这样在调试时就可以只看标准错误输出的错误信息
例如
重定向标准输出和标准错误,分别生成日志,这样就在出现错误的时候,就可以直接查看错误日志
进程和文件的关系
进程可以把数据给文件,文件也可以把内容给进程
进程可以修改文件的属性,文件的属性对进程也拥有限制
所以:
我们研究打开的文件,就是在研究进程与打开文件的关系
进程加载到内存之后,一定会打开至少3个文件(每个进程运行之后,都会打开标志输入,标准输出,标准错误3个文件)
打开的文件会被加载到内存,因为打开的文件会有多个
所以操作系统一定会把打开的文件管理起来
也就是对打开的文件:先描述[struct file
]再组织
即为打开的文件定义了一个结构体,里面存储打开文件的属性
`由操作系统定义的与读写有关等运行属性+磁盘中存储的文件的基本属性构成`
再使用合适的数据结构把所有的struct file装起来
这样对打开文件的管理,就变成了对数据结构的增删查改
操作系统中:
进程和文件是两个不同的模块,所以它们之间肯定不能强耦合
只能弱耦合:
每个进程只需要能找到自己打开的文件就行了,剩下的对文件的各种操作,都通过函数/系统调用接口完成
所以只需要在创建一个结构体变量,进程存储指向它的指针,用它来管理进程自己打开的文件
所以:
-
进程PCB中就存储了struct files_struct*类型的指针,指向一个自己对应的结构体变量
这个结构体就是:
struct files_struct
-
struct files_struct
里面创建了一个叫做文件描述符表的指针数组[struct file* fd_array[N]
] -
指针数组中存储了struct file*类型指针,
数组下标就是文件描述符
所以进程使用系统调用read和write等系统调用时:
- 就是进程通过在CPU上执行系统调用的代码找到该进程的PCB
- 再通过PCB里面的指针找到struct files_struct
- 再通过文件描述符进行数组访问,找到对应的文件对应的struct file
所以进程管理和文件管理就通过struct files_struct和struct file* fd_array[N]进行弱耦合
在用户层面文件描述符是访问文件的唯一方式,因为系统调用接口只能通过文件描述符来找到对应的文件
因为文件在磁盘中,用户只能通过操作系统的系统调用来访问它
所以任何语言访问文件的方式都是对系统调用的封装(封装是为了更好地供用户使用)
所以它们都必定会封装文件描述符等拱系统调用使用的信息
所以语言对文件访问的封装体现在两个方面
①对文件操作的系统调用函数的封装
②对文件属性(信息)的封装,并且一定对文件描述符进行了封装
例:
FILE是C语言库提供的结构体,它封装了文件描述符等属性信息
fopen函数对open进行了封装

操作文件的系统调用
因为文件是存储在磁盘中的
所以任何语言想对文件进行操作,都必须通过操作系统的系统调用接口先访问到磁盘
进而访问文件,才能把文件读取到内存
所以任何语言的文件接口,底层都必须封装对应的文件类系统调用
系统调用函数:open
头文件:sys/types.h
,sys/stat.h
,fcntl.h
返回值:int类型
①成功,就返回文件描述符
②失败,就返回-1
参数表:(const char*p,int f,mode_t mode)
①p:带(或者不带)路径的文件名
②f:位图
宏标记位:
1,O_RDONLY:只读
2,O_WRONLY:只写
3,O_RDWR:读写
4,O_CREAT:没有对应文件时,创建文件
5,O_APPEND:追加
6,O_TRUNC:打开文件时,清空文件内容
可以通过按位或,一次传递多个标志位宏
③mode:文件的最初起始权限[即减umask之前的权限],以0+3个八进制位数字表示
权限使用分两种情况 :
1,要打开的文件已经存在,就可以不传
2,要打开的文件不存在,就要传,不然创建出来的文件的权限就是乱码
因为操作系统中创建文件和权限是两个不同的分支功能
操作系统对它们进行了解藕,所以不能在创建文件的同时给予它起始权限,只能先指定文件的起始权限,再创建文件
系统调用函数:umask
头文件:sys/types.h
,sys/stat.h
参数:mode_t mode
以0+3个八进制位数字表示
作用:将自己这个进程的权限掩码设置成传入的参数
系统调用函数:close
头文件:unistd.h
参数:int fd
即文件描述符
作用:关闭文件
系统调用:unlink
头文件:unistd.h
返回值:int类型
①成功返回0
②失败返回-1
参数表:
path:带路径的文件名
作用:删除一个指定路径的文件
系统调用函数:fsync
头文件:unistd.h
参数:int fd
即文件描述符
作用:刷新文件描述符为fd的文件的内核缓冲区
系统调用函数:read
头文件:unistd.h
返回值:size_t n
①n>0,表示读取到了多少个字节
②n=0,表示只读取到了文件结尾标志或者文件关闭了
或者连接断开了
为什么不是文件内核缓冲区没有数据了呢?
因为read是阻塞式读取的
所以如果缓冲区没有数据read就阻塞了,根本不会返回
③n<0,读取出错或者read函数异常
参数表(int fd,void*buf,size_t count)
①fd:文件描述符
②buf:读取缓冲区
③count:期望读取到的字节总数
作用:读取文件内容
系统调用函数:write
头文件:unistd.h
返回值:size_t n
①n>0,表示写入了多少个字节
②n=0,表示文件关闭了或者连接断开了
为什么不是文件内核缓冲区满了呢?
因为write是阻塞式写入的
所以如果缓冲区满了,write再写就阻塞了,根本不会返回
③n<0,读取出错或者write函数异常
参数表(int fd,void*buf,size_t count)
①fd:文件描述符
②buf:写入缓冲区
③count:写入的字节总数
作用:用文本写入方式向文件写入
所以:
语言中的文件接口fopen,fclose,read等底层都会封装open,close,read等系统调用
如何理解Linux下一切皆文件
电脑的硬件也有很多,操作系统作为软硬件的管理者
以硬件为例:
电脑的硬件也有很多,操作系统作为软硬件的管理者
必然需要对所有硬件也进行管理
管理方案还是:先描述,再组织
也就是给所有硬件也设置一个结构体,通过驱动程序获取硬件信息,初始化出结构体变量,再把所有的结构体变量放进数据结构中
内存与外设之间最主要的操作就是IO(读写)操作【其他操作也一样,多搞几个函数指针的问题
】
所以所有的外设都得向操作系统(内存)提供读写方法
因为每个硬件的具体情况不同,它们实现读写的实现肯定是不同的(即函数的实现不同)
但是提供的接口都叫读(写)接口[read()
和write()
]
所以操作系统只需要,在struct file中设置函数指针,分别指向read()和write()等接口
就可以通过函数指针使用所有的外设的读写函数[根据地址的不同,即可使用不同的外设定义的不同函数
]
此时就能做到:
通过struct file对外设进行读写时
不需要管外设是谁,不管它读写函数的函数实现是什么,只需要知道要读什么,写什么
这不就是在struct file的上层把外设当成文件了吗?
操作系统的open,close,read等系统调用的实现就是
找到对应的struct file结构体变量,通过里面的函数指针调用对应外设根据自己的实际情况
实现的函数
所以站着进程角度,Linux下一切皆文件
因为用户的一切操作(命令)都是由进程执行,所以用户也认为一切皆文件
这不就是使用C语言实现的多态吗!!!
IO的基本过程
写入
使用write进行写入时:
-
通过文件描述符找到对应的文件的
struct file
结构变量 -
找到文件的
struct flie
中的一个指针指向的内核缓冲区,再把要写入文件的内容拷贝进去 -
操作系统认为
可以写入时[所以系统调用write的作用其实只有拷贝到内核缓冲区,没有写入到文件]再使用文件的structfile中对应的写接口的函数指针,把内核缓冲区中的数据写入到磁盘文件
操作系统认为要可以写入时,在windons中其实一般是用户编写文件过程中,点击了保存按钮
所以我们使用键盘向文件进行写入时,其实一直在使用write这样的系统调用把数据写到缓冲区中,点击保存时才是写入文件。
如果操作系统认为当前IO压力不大,操作系统觉得挺闲的也有可能内核缓冲区出现数据,它就刷新
读取
使用read进行读取时:
-
通过文件描述符找到对应的文件的
struct file
结构变量 -
找到文件的
struct flie
中的一个指针指向的内核缓冲区,再把要读取的数据拷贝到用户指定位置 -
如果内核缓冲区中还没有要读取的数据,就会尝试使用
struct flie
指向的函数指针集合中的读取的函数指针调用读函数
把数据先从外设读取到内核缓冲区,再拷贝到用户指定位置这种情况就有可能导致进程阻塞,最常见的就是:
要从键盘读取数据,但是用户又没有输入,就会阻塞
所以打开文件之后
1,磁盘中的文件的属性交给struct flie用来初始化了
2,磁盘文件的部分(或者全部)内容(因为预加载/需要使用)就会被写入到内核缓冲区中
修改
-
先检查用户想要修改部分是否已经在内核缓冲区中
如果在:
就直接把这部分拷贝到对应执行修改任务的进程中,进行修改
如果不在:
就调用磁盘的读接口,把要修改部分读取到内核缓冲区中,再拷贝到用户的进程那里进行修改
-
修改完成之后,再拷贝回内核缓冲区,等到合适的时候,再调用磁盘写接口写入到磁盘中
为什么要有这个文件内核缓冲区?
①写入外设角度:
数据在内存中"流动"比在内存和外设之间快的多
如果每次写入操作都之间写入到外设,就会多次IO,效率低
有了缓冲区,内存多次写入时,就可以把数据都拷贝进缓冲区中,时机合适再写入,此时只需要向外设写入一次就行
②从读取角度:
用户打开文件之后,可以一次性读取较多的文件内容到缓冲区中,也就是预加载
虽然用户要使用的数据不一定在里面,但有的大概率在里面
就算不在,也可以把用户想要的数据附近的数据全部加载进来,这样命中率也能提高
所以内核缓冲区的存在能大大提高IO的效率
每一个struct flie都有一个自己的内核缓冲区
一个文件只有一个内核级缓冲区
在内存中,一个文件的内容和属性不会被重复加载
即:一个被打开的文件只会出现一份inode,一份内核级缓冲区
所有打开这个文件的进程,共用一个inode和文件内核级缓冲区
因为不同进程打开同一个文件时,struct file
是不一样的
打开方式和读写位置等文件操作信息是记录在struct file中的
而且系统会对文件的内核缓冲区使用同步互斥机制,保证数据一致性
所以共享同一个缓冲区没问题