基础I/O
文章目录
0 . 预备
文件是什么?
文件 = 文件内容 + 文件属性。
所以对于文件的学习,就是要学习文件的内容以及属性。
怎么才能访问文件?
文件存放在磁盘上,只有os能够访问,所以如果用户想要自己访问文件,那么os必须提供文件接口。
C语言等语言中的文件操作是系统提供的文件接口吗?
不是,语言接口是对系统提供的文件接口的封装。不同的语言由于语法的不同,文件操作也会不同,所以这并不是系统接口。
学习系统接口的意义
语言接口有多套,系统接口只有一套,学一套会十套。
不同的系统,系统接口相同吗?
显然不同,所以,如果我们自己写代码直接使用系统接口,那么这段代码的可移植性是不好的。C语言具有可移植性是因为C语言源代码将所有平台代码都实现了一遍,你用啥平台,他就放啥代码。
Linux认为一切皆文件。
狭义的文件就只是仅仅指文件,但是Linux认为只要能够写入,能够输出,即具有 I/O属性的设备都叫做文件。
0.1 C语言接口
fopen
fclose
fscanf
fprintf
…
C语言文件接口分为两部分,第一是打开文件的方式,第二是对文件进行的操作。
打开文件的方式有很多种
打开代码 | 意义 | 若指定的文件不存在 |
---|---|---|
r(只读) | 打开一个已经存在的文件进行读取 | 出错 |
w(只写) | 打开一个文件进行写入 | 新建一个文件 |
a(追加append) | 打开一个已存在文件并在末尾进行添加 | 出错 |
目前只列这么多种,还可以进行组合,也可以添加一个“+”增加一个属性。
0.2 系统接口
上面的所有C语言接口都是封装系统调用接口实现的。
我们要学的系统调用接口有四个
open:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XpB3CUMh-1672026572674)(E:\比特\笔记\文件系统.assets\image-20221208164451327.png)]
open函数返回一个int类型的值,叫做 fd, 也就是我们的文件描述符,若fd为-1表示打开失败,否则成功。
fd 的值为 0, 1, 2分别代表标准输入,输出和错误。
C语言上的stdin, stdout和stderror是对 fd做封装的结构体。
open函数有两种函数原型。这两个函数的区别是是否具有mode这个参数,mode代表文件的权限,mode和之前的chmod一样,通过给八进制数来设置权限,比如说mode设为 0666 ,就是rw-rw-rw-,但是同时也要考虑umask的影响,可以通过umask这个系统接口将umask的初始值设为 0.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nhxX8PaC-1672026572676)(E:\比特\笔记\文件系统.assets\image-20221208203552520.png)]
open函数的第二个参数是 flag,代表操作选项,从文档中可以知道open有
O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读+写), O_TRUNC(如果指定文件存在,就清零,不存在,就创建新的空文件)等等选项,这些选项的组合是通过 ‘或 ‘操作实现的,比如想要实现只读 + 创建新文件或清零就这样写
umask(0); //设置umask int fd = open("hello.txt", O_RDONLY | O_TRUNC | O_CREAT, 0666);
close
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMoRdN2J-1672026572676)(E:\比特\笔记\文件系统.assets\image-20221208204505239.png)]
close的用法很简单,直接将fd传进去就好,
read
从文件中读出数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgmj6pJ0-1672026572677)(E:\比特\笔记\文件系统.assets\image-20221209120459792.png)]
函数参数:
fd : 表示打开文件的文件描述符。
buf:表示将要读入的数据结构
count:表示要读入的个数
返回值:
返回值的类型是 ssize_t,这是一个正整形,表示的是实际读入的字符个数。
write
1 . 文件描述符 (fd)
在上面的fd就是我们要讲的文件描述符,为什么要有文件描述符,文件描述符是什么呢?
一个进程可以打开多个文件,打开的文件多如牛毛,os为了将其管理起来,也要“先描述,再组织”。
实际上os用了指针数组的方式来管理文件存放的指针指向一个个文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awGMEy09-1672026572677)(E:\比特\笔记\文件系统.assets\image-20221209115652539.png)]
files_struct中存储着一个个指向已经打开的文件,所谓的文件描述符就是这个指针数组的下标。
1.1 fd的分配规则
fd的分配规则是分配最小的,且没有被打开的文件描述符。
比如如果0, 1, 2,如果被stdin,stdout和stderr占用了,那么新打开的文件就会被安排上后面的数字中的最小值。
当然,如果将0,1,2中的任意一个close掉,比如close(0),后面的新加入内存的文件就会被分配0(因为此时0是最小的,且没有被打开)。
//狸猫换太子
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/stat.h>
4 #include <sys/types.h>
5 #include <string.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 umask(0);
11 close(1);
12 int fd = open("login.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
13 if (fd < 0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 printf("%d\n", fd);
20 fflush(stdout); //如果不刷新缓冲区就不能写入我们的文件
21 close(fd); 22 return 0;
23 }
这段代码把stdout关闭了,fd就被分配到stdout的位置,而printf和fprintf都是向stdout内写入信息,自然就写入到了login.txt中。也就是说file_struct这个指针数组中下标为1的位置存放的指针实际上指向的不是显示器,而是我们自己的文件。而这里也就是我们的输出重定向的原理。
要注意的是必须加上fflush或者注释close(fd),不然写入不到我们的文件中,原因后面讲。
重定向
在os内部,更改fd对应的内容的指向,就可以实现。
上面讲的仅仅是重定向的原理,实际上我们没必要使用重定向的时候每次都要close原来的fd。在Linux中有一系例如系统调用接口:dup,其中最常用的是dup2.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UajKlv4v-1672026572678)(E:\比特\笔记\文件系统.assets\image-20221209162544152.png)]
dup2函数原型如下
int dup2(int oldfd, int newfd);
通过dup2函数将file_struct中的oldfd下标所在的指针更换成newfd下表所在的指针,依次来实现重定向的功能。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
if (argc != 2) return 2;
umask(0);
//重定向接口
int fd = open("login.txt", O_WRONLY | O_CREAT | O_A PPEND, 0666);
dup2(fd, 1);//让1中内容和fd中内容一致
printf("fd:%d\n", fd)
printf("%s\n", argv[1]);
return 0;
}
//代码输出结果:
//fd:3
//if_the_swap_success?
由上述代码输出结果可以看出,dup2并不会改变oldfd的值,仅仅是改变了newfd的值,所以可以的话,尽量手动关闭oldfd。
2. 一切皆文件
之前一直说Linux下一切皆文件,但是为什么这么说呢?之前一直都只是感性的认识,凭什么这么说呢?
思考一个问题:用C语言如何实现面向对象方法 ?
在C++的类中不仅能包含成员变量,还能够包含成员函数,而在C语言中,可以使用函数指针这样的方式来完成一个面向对象的结构。
struct
{
//成员变量
int (*read)(int n, void *buf);//函数指针,指向方法
int (*write)(int n, void *buf);
};
而我们说一切皆文件,也就是把键盘,显示器,磁盘,网卡等外设全部一视同仁了,但是这些设备他的物理结构就是不一样的呀,所以他们的访问方式必须是不一样的,那为什么还说一切皆文件呢?
答案就在于上面的struct,尽管你们的读写方式是不同的,但是我同样的可以用一个struct来描述你,只不过函数指针指向你特定的函数,所以在上层看来,所有的外设都一样,都可以用这个struct来描述。同时struct的数量多起来后,也是需要“先描述,再组织”的。
所以才说,Linux下 “一切皆文件”。
3. 缓冲区
缓冲区其实本质上就是一段内存空间,缓冲区存在的意义就是提高效率,他属于是拒绝了一个一个数据刷新的策略,是积攒一些数据后再刷新。
因此缓冲区其实很像快递。而缓冲区的刷新策略和快递的发货策略也很相像
-
立即刷新
-
行刷新(刷新\n之前的所有数据)
-
满刷新数据满了之后再刷新
-
特殊情况:
1.用户强制刷新:fflush
2.进程退出
一般而言 行缓冲的的设备文件 – 显示器
全缓冲的设备文件 — 磁盘文件
但是,所有的设备永远都倾向于全缓冲,因为这样可以有效减少IO操作(IO操作太耗费时间)。
一个例子:
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7
8
9 int main()
10 {
11 //C语言提供接口
12 printf("%s", "hello printf\n");
13 fprintf(stdout, "%s", "hello fprintf\n");
14
15 //os提供接口
16 const char* s = "hello write\n";
17 write(1, s, strlen(s));
18
19 fork();
20 return 0;
21 }
这段代码在运行之后向显示器上打印,那么结果是这样
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGbJP4Wu-1672026572678)(E:\比特\笔记\文件系统.assets\image-20221209185747629.png)]
如果有fork函数,结果则截然不同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5G8wCvk-1672026572679)(E:\比特\笔记\文件系统.assets\image-20221209185814010.png)]
可以看到,os提供的接口没有重复打印,但是C语言提供的接口却重复打印了
还记得之前说的那个close为什么要加fflush或者注释close的疑问吗?
之前代码再粘贴一下
//狸猫换太子
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/stat.h>
4 #include <sys/types.h>
5 #include <string.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 umask(0);
11 close(1);
12 int fd = open("login.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
13 if (fd < 0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 printf("%d\n", fd);
20 fflush(stdout); //如果不刷新缓冲区就不能写入我们的文件
21 close(fd); 22 return 0;
23 }
这个时候调用的是printf函数,向stdout的缓冲区中写入了对应的数据,但是此时stdout对应的是磁盘文件,虽然写入了‘\n’,但是仍然不会刷新,当使用close将文件刷新后,stdout的缓冲区数据就无法刷新了。因此文件中没有内容。
从上述现象中可以知道,缓冲区绝对不是os提供的,而是C标准库提供的,因为如果是os提供的,那么系统接口应该也被重复。
第一种结果是向显示器中打印,显示器采用的是行刷新的策略,所以在fork执行的时候,缓冲区已经完全刷新了,所以此时并不会出现重复。
第二种结果中我们进行了输出重定向,向磁盘文件上写入,而磁盘文件是满刷新,fork执行时,明显缓冲区还不会刷新,所以此时如果父子进程退出,就会进行缓冲区刷新,缓冲区刷新要进行写时拷贝,同时由于缓冲区而由于缓冲区是C语言的规定,所以只有C语言的接口受限制,os的接口并不受控制。
缓冲区在哪?
事实上,在之前提到的每个文件都具有的文件结构体file_struct中,每次fputs,fprintf啥的都是先写入到缓冲区中、根据不同的缓冲刷新策略进行刷新。
stderr —— 2 的辨析
1 和 2对应的都是显示器文件,但是它俩是不同的,如同认为一个显示器文件被打开了两次。做一个代码测试
由代码测试可以知道,我们对1做重定向并不会影响2,所以其实1和2是独立的。
perror:perror函数会根据全局的错误码自动输出对应的错误信息。
errno:全局变量,表示错误码。包含在头文件errno.h中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtXMSqLP-1672026572679)(E:\比特\笔记\文件系统.assets\image-20221212213217417.png)]
strerr:错误信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HfeWwGjH-1672026572680)(E:\比特\笔记\文件系统.assets\image-20221212213133150.png)]
4. 没有被打开的文件 —— 磁盘文件
学习目标
- 如何对磁盘文件进行分门别类的存储,用来支持更好的存取
- 了解磁盘
4.1 磁盘结构
4.1.1 磁盘的物理结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBj4o2Xy-1672026572680)(E:\比特\笔记\文件系统.assets\20200330190841250.png)]
盘面:俯视看磁盘好像只有一个面,但是侧面观看就会发现一个磁盘具有许许多多的磁片。盘面看起来是光滑的,但实际上他是由许许多多个小磁铁组成的(因为磁铁具有二面性,符合计算机的0,1属性),磁头通过改变磁铁的南北极朝向写入数据。
磁头: 上图的 Read / write heads,磁头和盘面之间有一定的距离,属于是悬浮在盘面的上空,通过静电效应改变磁铁朝向。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2ydn0GL-1672026572680)(E:\比特\笔记\文件系统.assets\v2-7ef4dd23b909affc4c5529bad4b0564b_r.jpg)]
磁道:磁道指上图的一个同心圆,每一个同心圆被称为一组磁道,数据就存放在磁道上。(所以并不是整个盘面都能存放数据)
扇区:磁道上的一段弧段被称为扇区,磁盘的读写以扇区为基本单位。扇区的大小为512字节。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JBPYvcum-1672026572681)(E:\比特\笔记\文件系统.assets\20191109164411288.png)]
柱面:柱面就是以一个磁道为底面,所有磁片的厚度为高度的一个柱形。
如何寻找到一个扇区进行写入?
chs方式:
- 找到是哪一个面(磁头)
- 找到是哪一个磁道(柱面)
- 找到是哪一个扇区
通过这样的方式可以找到磁盘上的任意一个扇区进行写入。
4.1.2 磁盘的逻辑结构:
磁带示例:一卷磁带扯长开来就是一个线性结构,一条长带
–> 将磁盘盘片抽象成线性结构 --> 数组。(LBA)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yyv94txT-1672026572681)(E:\比特\笔记\文件系统.assets\image-20221213115120883.png)]
找到扇区的位置 --> 找到数组中对应的位置
对磁盘的管理 --> 对磁盘数组的管理
将磁盘进行划分 --> 对磁盘数组的划分
对磁盘的管理 --> 对磁盘数组小分区的管理
因此只要管理好一个个小分区,整个磁盘就能够管理好。
4.2 磁盘的管理
4.2.1 磁盘区间划分管理
接下来要讲的就是关于小分区的管理,如下图所示,一个大分区被分成一个个小分区,这些小分区又被分成更小的分区,只要管理好这些小分区,其他的分区都采用类似的方法进行管理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYP8uqE4-1672026572682)(E:\比特\笔记\文件系统.assets\image-20221213101250127.png)]
上述的一个Block_Group就是一个文件系统。
虽然磁盘以扇区(512字节)为基本单位,但是os和磁盘的IO操作的基本单位是4KB, 这是因为512字节太小,会增加IO操作次数降低效率。而且如果os的基本单位和磁盘一样,那就是强耦合,因为如果基本单位一样的话那么磁盘基本单位一变,os也要跟着变,硬件和软件联系紧密,强耦合。
Boot block :存放启动信息
Super blocks:存放的是整个分区,即上图的**第一个矩形(包括所有的Block group和Boot block)**的文件系统属性信息,比如有多少个块组,每个块组的使用情况,但是为甚么整块的信息要放到一个分区内呢?这是因为要防止丢失数据,磁头容易刮花磁片,如果一个分区的Super Blocks损坏可以用另一个区域的Super Blocks进行拷贝恢复。并不是每一个分组都有,前几个会有。
Data blocks:存放对应文件的内容,基本单位是bolck,一个block大小为4KB(8 * 512字节), 也叫做块。
inode table:存放对应文件的属性,基本单位是inode,由一个个的ionode组成table,一个inode的大小是128字节,存放文件的inode编号,文件属性,存放当前文件的块组的编号等内容。
Block Bitmap : 用来管理Data Blocks,假设Data Blocks中有n个块,那么 Bitmap中就有n个比特位,这些比特位是1就表示该block被占用,是0表示可使用。
inode Bitmap:和Block Bitmap类似,用来管理inode Bitmap,如果有n个inode,那么Bitmap中就有n个比特位,是1表示该inode空间已经被占用,0表示可使用
Group Describer Table:存储inode和block的描述信息,例如: 总共有多少个inode,目前的使用情况;总共有多少个block,目前的使用情况。
一个文件对应着一个inode,一个inode编号。
但是,文件大部分不可能只有4kb,因此,一个文件绝大多数情况下不可能只占用一个block块,那么该如何找到该文件对应的内容呢?
在inode中就存储了这个文件占用的块数组的编号
struct inode
{
int _inode;//inode编号
//...文件描述信息
int _blocks[20]; //存储对应文件占用的Block的编号
};
所以只要找到inode编号就能够访问文件属性以及内容了。
但是还有一个问题,如果文件特别大呢?
其实Data block中也不是所有的data block只能存文件数据,也可以存其他块的编号,要知道一个block有4KB,能存储的指向文件的指针的数量就非常可观了。
4.2.2 inode
如何知道inode呢?
在Linux的inode属性中,是没有文件名这一内容的!!
那么文件名去哪了呢?
答案是在上级目录的data block中!!!
目录的内容,也就是目录的data blocks中存放的是文件名 和 inode 的映射关系,在一个目录下文件名是唯一的,而同时在一个文件系统中inode也是唯一的,所以他们互为Key值。
创建一个文件,文件系统做的事
touch test.txt
首先在inode Bitmap中遍历找到第一个为0的inode位,置为1.对Block Bitmap做同样的事情,如有必要对blocks进行数据写入。根据目录的inode找到目录的Data Blocks,将这个文件inode编号以及文件名写入目录的Data Blocks。
删除一个文件,文件系统所做的事
rm -f test.txt
根据文件名,在上级目录中找到自己的inode,根据inode找到对应的inode bitmap和block bitmap,将该文件对应的位图置为0。然后在目录的data blocks中文件名和inode的映射关系去掉。
查看一个文件,文件系统所做的事
cat test.txt
inode, data blocks所占用的空间是固定的
所以就会导致还有空间但是无法创建文件。
5. 软硬链接
软硬链接通过 ln 命令执行
#软链接
ln -s testLink soft.link
#硬链接
ln testLink1 hard.link
软硬链接的本质区别:
是否具有独立的inode,软链接具有独立的inode,硬链接没有。
所以软链接是一个独立的文件,而硬链接不是。
软链接:
有一个独立的文件,上述指令生成一个链接文件testLink --> soft.Link,这个testLink实际上指向的是soft.Link的路径,执行testLink相当于执行soft.Link,所以软链接相当于一个快捷方式。
硬链接:
没有独立的文件,硬链接就是在指定的目录的Data Blocks中,写入硬链接文件名和指定的文件的inode的映射关系。硬链接文件和原文件共用一个inode。硬链接相当于起别名。
在属性中有这么一个数字,他在删除文件前是2,删除文件后是1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2R2wS2cl-1672026572683)(E:\比特\笔记\文件系统.assets\image-20221215165556277.png)]
删除前
rm -f test
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4R7FEpO-1672026572683)(E:\比特\笔记\文件系统.assets\image-20221215165813830.png)]
删除后
这个数字叫做硬链接数。
前面讲过的是一个文件只有一个inode,inode是唯一的,这里要完善一下说法:inode对于文件是唯一的,但是一个inode可以对应若干个文件。
在inode的结构中,有一个计数器,用来计算使用这个inode编号的文件数,这个数就是硬链接数。所以其实我们删除一个文件其实是对这个计数器减减,当硬连接数为0时,这个inode空间就被标为可用了。
unlink 系统调用
unlink soft.link
unlink hard.link
unlink 清零对应文件的硬链接数。
默认创建目录,硬链接数默认是2,这是因为在目录中存在两个隐藏文件: . 和 … 。
[外链图片转存中…(img-2R2wS2cl-1672026572683)]
删除前
rm -f test
[外链图片转存中…(img-S4R7FEpO-1672026572683)]
删除后
这个数字叫做硬链接数。
前面讲过的是一个文件只有一个inode,inode是唯一的,这里要完善一下说法:inode对于文件是唯一的,但是一个inode可以对应若干个文件。
在inode的结构中,有一个计数器,用来计算使用这个inode编号的文件数,这个数就是硬链接数。所以其实我们删除一个文件其实是对这个计数器减减,当硬连接数为0时,这个inode空间就被标为可用了。
unlink 系统调用
unlink soft.link
unlink hard.link
unlink 清零对应文件的硬链接数。
默认创建目录,硬链接数默认是2,这是因为在目录中存在两个隐藏文件: . 和 … 。
. 表示当前目录,当然也就是当前目录的别名,所以 . 是当前目录的一个硬链接。