目录
- 1 什么是文件?什么是文件操作?
- 认识系统接口open
- 什么是文件描述符
- 认识Linux底层进程如何打开的文件映射关系
- 重定向的本质
- 理解软硬链接
- 扩展问题
1 什么是文件?什么是文件操作?
文件 = 文件内容 + 文件属性(文件属性也是数据,即使你创建一个空文件,也要占据空间)
文件操作 = 文件内容操作 + 文件属性操作(有可能,在操作文件的过程中,既改变内容,又改变属性)
不管是学习语言还是学习操作系统,IO流是我们学习过程中不可获取的一个阶段,在这一部分我们会学习打开文件、读写文件等操作。
那所谓的“打开文件”,究竟是在干什么呢?其实就是将文件的属性或内容加载到内容中。
我相信大多数人的入门语言都是c语言,那我们就以c语言的文件操作进行举例;
如何理解printf?为什么调用printf就能往显示屏中打印内容呢?
其实printf是封装了系统的接口,然后由系统接口调用显示屏的驱动方法,然后进行打印
其实不管是C语言也好,Java也好,其他语言也罢,大多数的文件操作都是封装了系统接口。
封装的原因
- 原生系统接口使用成本比较高
- 语言不具备跨平台性(每个操作系统的接口可能不一样,写的代码可能只能在一个平台上跑)
封装是如何解决跨平台的问题的呢?
以c/c++方式举例:穷举所有底层接口 + 条件编译
在Linux系统调用接口中,我们打开文件使用open、关闭文件close、写入write、读取read。那这些接口和C中库函数接口有什么联系呢?我们可以这样理解:C中调用得这些库函数底层一定封装了系统调用接口,可以认为fopen底层调用open,fclose底层调用close,fread底层调用read,fwrite底层调用write。我们在windows中打开文件,windows底层也有一套自己的windows相关的api系统接口,当我们在windows使用C的库函数时,C调用的就是windows下的系统接口。这样在语言层面上就实现了跨平台性。
认识系统接口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: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
int main()
{
umask(0);
int fda = open("loga.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
int fdb = open("logb.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("fda:%d\n",fda);
printf("fdb:%d\n",fdb);
close(fda);
close(fdb);
return 0;
}
运行结果:
fda:3
fdb:4
什么是文件描述符
OK,那前面描述怎么多,到底什么是文件描述符呢?其实系统接口open的返回值就是文件描述符。
Linux系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,OS向进程返回一个文件描述符,文件描述符就是OS为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,基本上所有执行I/O操作的系统调用都会通过文件描述符。简单来说,文件描述符可以唯一标识一个新打开的文件;类似学号,身份证号可以唯一标识一个人。
OK,引出概念后,我们来解决下面几个问题
上面那个例子中,为什么运行结果是3,4;或者说为什么是从3开始?
因为操作系统默认已经打开0,1,2号文件
0表示:标准输入,键盘
1表示:标准输出,显示器
2表示:标准错误,显示器
认识Linux底层进程如何打开的文件映射关系
** 为什么文件描述符是像0,1,2,3,4,5…,你见过什么样的数据,是这样子的?**
当然是数组的下标就是这样子的啦
首先在一个进程可以打开多个文件(就像我们写代码可以打开多个文件一样),那这么多文件,我们的OS要怎么管理呢?
一个文件被打开,在内核中,要创建该打开的文件的内核数据结构叫做struct file
struct file
{
//包含了我想看到的文件大部分内容+属性
struct file *next;
struct file *prev;
}
再讲这个结构体file用类似链表的数据结构给串起来,所以对被打开的文件的管理,转化为对链表的增删改查
** 那进程如何和打开的文件建立映射关系呢?**
看下图
首先task_struct(进程控制块PCB)里面有一个字段是struct files_struct* files;files_struct里面有一个文件描述符表,其实就是一个array[]数组,里面存着结构体file的地址;当我们用户要打开一个文件时,先给你创建struct file这个结构体,初始化file内部属性还有函数指针指向的对应的方法,并且在当前进程的文件描述符表里分配一个没有被使用的下标,将新文件的地址(也就是结构体file的地址)给文件描述符表;然后会将这个新文件对应的文件描述符表的下标返回给用户,后面用户在调用read,write,一定传入了对应的文件描述符(fd),根据这个文件描述符就能找到对应的文件,对它做相应的操作。
那肯定有人问了,0,1,2 对应stdin,stdout,stderr 对应 键盘,显示器,显示器(这些都是硬件啊)也用你上面的struct file 来标识吗?
是的,在Linux下一下皆文件
在Linux下一切皆文件,结构体struct file里面有函数指针,指向的就是对应的IO方法(read,write),像磁盘啊,显示器啊,键盘啊,这些硬件,调用他们要调用对应设备的驱动方法,所以struct file的函数指针,指向这些对应设备的驱动方法。比如说标椎输出2号文件,结构体的函数指针就是指向显示器的驱动方法,将数据写到2号文件,也就是显示到显示屏上。
所以一切皆文件,举个例子,把显示屏看做文件,我们往显示屏打印数据,不就是往文件里写东西而已。
你可以验证一下上面的理论,先把标准输出1号文件关闭了,再打开log.txt文件,根据fd的分配规则,新的fd值一定是1;所以此时调用printf();本来printf是打印到1号文件,也就是显示屏;但是现在打印到log.txt文件里取了(当然这个过程也叫做重定向)
文件描述符的分配规则:从头遍历数组fd_array[],找到一个最小的,没有被使用的下标,分配给新的文件
int main()
{
close(1);
//根据fd的分配规则,新的fd值一定是1
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd < 0)
{
perror("oppen");
return 1;
}
//printf-》stdout->1->虽然不在指向对应的显示器了,但是已经指向了log.txt的底层struct file 对象
//本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向
printf("fd:%d\n",fd);
//为什么必须要fflush一下
fflush(stdout);
close(fd);
}
重定向的本质
修改文件描述符fd下标 对应的struct file * 的内容 (将其换成目标文件的地址)。
重定向具体操作
int dup2(int oldfd,int newfd);//duplicate重定向
int main()
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 0;
}
dup2(fd,1)>=0
//本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向
fprintf(stdout,"打开文件:%d",fd);
fflush(stdout);
close(fd);
return 0;
}
理解软硬链接
在讲软硬链接之前,我们得先了解一下什么是inode。
我们刚才说的,打开的文件是内存级别的,实际上大量的文件还在磁盘上,这些文件和多,很杂,我们的文件系统就要对这些进行管理。
那么一个硬件(磁盘)怎么跟我们的OS产生联系呢?
把磁盘想象成为线性结构,当做500GB大小的数组,然后对磁盘的管理,转化为了对数组空间的管理。但是一次性管理500GB大小的数组不太可能,所以就对500GB的数组进行分区,一个区100GB,一个区还是太大,在对区进行分组;一个组10G;组内在细分成块,然后内核对这些块进行描述,比如有什么属性,属于哪个组;然后就转为对这些块的管理。
Linux 系统中就有一个名为 superblock 的 “硬盘地图”。 Linux 并不是把文件内容直接写入到 superblock 中,而是在里面记录着整个文件系统的信息。superblock里面就有一个叫inode的。
文件= 内容 + 属性——都是数据——都要存储
Linux采用的是将内容和属性数据分开存储的方案
内容一般放在(一块块)block中(4kb),属性数据一般放在inode中(inode说人话就是磁盘上的另一份空间,一般这份空间是128字节)
Linux 把每个文件的权限与属性记录在 inode("索引节点:index node ") 中,而且每个文件占用一个独立的 inode 表格,该表格的默认大小为 128 字节。
里面记录着如下信息 :
-
文件的访问权限(read、write、execute)
-
该文件的所有者与所属组(owner、group)
-
该文件的大小(size)
-
该文件的创建或内容修改时间(ctime)
-
该文件的最后一次访问时间(atime)
-
该文件的修改时间(mtime)
-
文件的特殊权限(SUID、SGID、SBIT)
-
该文件的真实数据地址(point)。
在 Linux 系统中 ,inode 号才是文件的唯一标识而非文件名。文件名只是为了方便人们的记忆和适用
那我现在知道了,inode就是文件的唯一标识,用来存储文件的属性的,那软硬链接到底是什么呢?
硬链接(hard link) : 可以将它理解为一个 “指向原始文件 inode 的指针”,系统不为它分配独立的 inode 和 文件。所以,硬链接文件与原始文件其实是同一个文件,只是名字不同。我们每添加一个硬链接,该文件的 innode 连接数就会增加 1 ; 而且只有当该文件的 inode 连接数为 0 时,才算彻底被将它删除。因此即便删除原始文件,依然可以通过硬链接文件来访问。需要注意的是,我们不能跨分区对文件进行链接。所以硬链接可以用来防止误删。
软链接(symbolic link) : 等同于 Windows 系统下的快捷方式。仅仅包括所含链接文件的路径名字。因此能链接目录,也能跨文件系统链接。但是,当删除原始文件后,链接文件也将失效。
软硬链接的区别:软链接是一个独立文件,有自己独立的inode和inode编号;硬链接,不是一个独立的文件,它和目标文件使用同一个inode
扩展问题
-
什么是硬链接数??
此时的inode编号,不是就一个“指针”的概念吗?硬链接数本质就是给文件inode属性中的一个计数器count,标识有几个文件名和我的inode建立了映射关系。简而言之,就是有几个文件名,指向我的inode(文件本身)
-
既然软链接是一个独立的文件,那它的文件内容是什么呢??
保存的是指向文件的所在路径
-
为什么创建普通文件,默认硬链接数是1;而创建目录,默认硬链接数是2呢??
普通文件的文件名,本身就和自己的inode具有映射关系,所以是1;
默认的空目录,有两个默认文件.和…,.表示当前目录,也指向这个目录,所以目录自己指向自己的inode,.也指向目录的inode,所以是2;而…指向的是上级目录的inode