文件操作
一、引入
我们知道每个语言都有自己的文件操作接口且各不相同,而OS访问磁盘中的文件必须要有一个系统调用接口 。所以上层语言不管如何变化,底层接口不会变。所以我们只需要学习底层接口就可以了。
二、系统调用接口
2.1 open与close
open
返回值:
close
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC: 覆盖写
注:这些都是宏,使用每个比特位作为标志位。
现在我们来创建一个文件:
使用
O_WRONLY
**运行后没有出现log.txt文件,那我们加上第二个宏选项:
O_CREAT
**
运行后:
可以看到权限上都是乱码。所以我们知道需要自己来添加默认权限。
2.2 write
2.3 read
三、文件描述符
3.1 分配规则
一个进程可以打开多个文件,所以OS需要把这些文件管理起来,而管理的方式就是使用文件描述符。
我们可以看到fd是从3开始,那么0,1,2呢?
默认打开的三个标准输入输出流:
三个标准输入输出流
stdin 标准输入(键盘)
stdout 标准输出(显示器)
stderr 标准错误输出(显示器)
引入一个概念:系统调用接口访问文件必须要用文件描述符。
可以看到,如果不加close(0),fd默认是3,而关掉0时,fd就变成了0。
所以我们就可以知道fd的分配规则:
从小到大寻找没有被占用的位置。
四、重定向
4.1 输出重定向
这个进程的意思就是把标准输出流关闭,使1(fd)的位置指向log.txt,所以他会把数据输出到log.txt中。
但是我们在log.txt并没有发现出现任何内容。这就是缓冲区刷新原则。
而这种本来应该往显示屏上打印却打印到文件中的操作我们称之为重定向。
系统给了我们一个重定向的接口:
**
dup2
**
**
int dup2(int oldfd,int newfd);
**
它的意思就是把oldfd的内容拷贝到newfd的位置,oldfd和newfd都指向同一个文件。
4.2 追加重定向
追加重定向其实跟dup2没有关系,而是在open处做修改:
4.3 输入重定向
这里就是把本来应该从键盘输入内容的重定向到文件中。
4.4 独立性
当我们用子进程进行程序替换的时候,会不会影响到父进程呢?
答案是不会,进程具有独立性。子进程的PCB会指向一个新创建的files_struct。
这里补充一点:
当我们重定向好一个文件后,如果发生了程序替换,并不会影响重定向的文件!
五、缓冲区
先来看一个现象:
正常输出:
重定向到文件中:
我们发现C语言接口输出了两次。
这里就跟缓冲区有关系了。
首先要知道缓冲区就是一块内存。
缓冲区的意义:
当我们想IO时,直接把数据拷贝到缓冲区,拷贝函数就可以返回了,接着运行后续程序。所以它可以节省进程数据进行IO的时间。
就像fwrite函数,我们可以把它理解为拷贝函数,它会将进程中的数据拷贝到缓冲区或者外设。
5.1 缓冲区刷新策略
现在我们有一块数据想要刷新到外设,是一次写入到外设还是多次写入外设效率高呢?
答案是一次写入效率最高,因为访问外设次数少。
所以缓冲区会结合具体的设备,定制刷新策略。
1️⃣ 立即刷新(无缓冲)
2️⃣ 行刷新(行缓冲 ——> 显示器)
3️⃣ 缓冲区满则刷新(全缓冲 ——> 磁盘文件)
当然会有特殊情况:
① 用户强制刷新(fflush)
② 进程退出(退出时一般会刷新缓冲区)
5.2 缓冲区位置
我们首先可以知道缓冲区一定不在内核中,因为如果在内核中,write也应该打印两次!!
我们之前讲的所有的缓冲区都指的是用户级语言层面提供的缓冲区。这个缓冲区在FILE结构体中。
FILE结构体:
5.3 现象解释
现在我们就可以对上述情况进行解释:
当我们没有重定向的时候,stdout默认对应的是行刷新,在fork之前C函数已经将数据打印到外设中了,此时FILE中就没有数据了。
当我们重定向时,写入的文件就不是显示器了,是普通文件,而普通文件对应的刷新策略是全缓冲,数据不足以写满缓冲区,此时执行fork后。创建子进程后,子进程退出时,数据会直接刷新。而刷新会引起写实拷贝!!,而父进程最后也会刷新一次,所以C语言接口会刷新两次。
而为什么write系统接口没有刷新两次呢?上述的过程都跟write无关。因为write没有FILE,用的是fd,没有C提供的缓冲区。
六、文件系统
我们知道磁盘的空间很大,所以要进行分区管理。
6.1 文件系统分区
6.1.1 分区图
6.1.2 介绍
文件需要存属性+内容
inode用来存储文件的属性,是固定大小的。每一个文件都有自己的inode,为了区分每个inode,他们都会有自己的id:
Date Block存储的是文件的内容
上面两个都有一个BitMap是用来记录存储情况的。
而GDT则是对应的宏观存储状态。
6.1.3 软硬链接
6.1.3.1 链接方法
软链接:
硬链接:
6.1.3.2 软硬链接区别
软硬链接的区别就是是否具有独立的inode
软链接具有独立的inode,可以当作独立文件
硬链接没有独立的inode
那么我们怎么理解硬链接呢?
当我们往文件写入数据后
可以看出写入数据后硬链接的大小会变化而软链接不会变。
得出结论:创建硬链接没有创建新文件。 它就是在指定路径下新增文件名和inode的映射关系。
一个inode可能被多个硬链接指向,所以inode有一个计数器,记录有多少个指向它,叫做硬链接数。
当我们删除log.txt时:
可以看到hard_file.link的硬链接数变成1。
我们可以得出结论:
当一个文件的硬链接数为0的时候,这个文件才算真正被删除。
软链接无法读取原来log.txt的内容,说明软链接不是用inode找文件,而是用的文件名(目标文件的路径)。
不同目录下的软链接:
硬链接数细节问题:
创建一个目录硬链接数直接变成了2,原因是**dir目录里面有
.
。**
所以
.
和
..
就是给目录建立的硬链接,这里要注意的是:
普通用户不能给目录建立硬链接。
七、动态库和静态库
7.1 概念
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
现在我们写了加法和减法的进程
1️⃣ 先把
.cc
文件变成
.o
文件
现在我们在另一个目录下创建main.c文件,为了使用我们可以把.o文件传过去。
但是光有.o文件不够,还需要.h文件
当我们不想让别人看看到源代码的时候就可以这么传递。
所以就出现了库的思想:
我们可以把所有的”.o“文件打一个包,形成一个库文件。而打包方式的不同又可以分为静态库和动态库。
7.2 静态库与静态链接
生成静态库
但是我们想要把库给别人使用,还得把.h文件也传过去。
接下来就可以发送库文件了。
发送完后需要进行安装
安装的意思其实就是把库拷贝到指定系统的路径下
但是此时我们编译不会成功,因为找不到.h文件和库文件
所以我们需要指明头文件路径
-I
,指明库文件路径
-L
。这里要注意如果我们要链接库就需要指明库名称。 而这个名称要去掉前缀和后缀。
gcc/g++默认都是动态链接,但如果只有动/静态库,那么就只会会动/静态链接,如果动静态库都有则会动态链接。
当然我们也可以把库文件拷贝到
/lib64/
,把头文件拷贝到
/usr/include/
这样编译的时候只用加库名称就可以了。
7.3 动态库与动态链接
要形成动态库还是一样要把源文件便变成
.o
文件,**与静态库不同的是要加
-fPIC
选项**
打包的时候要用g++使用
-shared
选项。
然后我们就可以创建一个目录把头文件和库包放进去再传给别人。
而链接的过程跟静态链接一样。
但是运行的时候会报错。因为动态链接和静态链接不一样,它在运行的时候操作系统需要找到库,但此时库不在系统路径下,操作系统无法找到。
那么怎么告诉操作系统库在哪里呢?
1️⃣ 环境变量
我们可以把路径添加到环境变量中。
2️⃣ 配置文件
我们把库文件路径写在任意一个配置文件中即可,为了方便删除我们可以创建一个新文件。
这里要注意使用ldconfig更新动态库缓存。
3️⃣ 软链接
当然我们也可以软链接到系统指定的目录下(/lib64
)。
7.4 fPIC
静态库会把代码拷贝到代码区,而这部分代码会使用绝对地址来查找。
而动态库则用的是fPIC:
fPIC就是产生位置无关码,就是相对位置。他会把动态库指定函数的地址写到可执行程序中。
在程序运行的时候会把动态库加载到共享区(在堆和栈之间)
版权归原作者 命由己造~ 所有, 如有侵权,请联系我们删除。