文件IO
复习文件接口
fwrite
fread
fprintf
fscanf
open
int open(const char * pathname, int flags);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
文件描述符fd
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
0,1,2对应的物理设备一般是:键盘,显示器,显示器
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数
组,每个元素都是一个指向打开文件的指针!
所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
文件描述符的分配规则
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
重定向的本质是在内核中改变文件描述符对应的下标内容
看下面的代码
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
输出的结果,不再是输出到显示屏上,而是将输出的内容放到了myfile文件里,因为我们关闭文件描述符1号时,我们创建打开文件,会发生重定向。
凡是向1号描述符写入的内容,都改为了写到myfile中,而不是写到标准输出中。
使用 dup2 系统调用进行重定向
int dup2(int oldfd, int newfd);
函数功能为将newfd描述符重定向到oldfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件
但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)
重定向前,若newfd已经有打开的文件,则会关闭
重定向后,oldfd和newfd都会操作oldfd所操作的文件
这里强调一点,重定向改变的是文件描述符指向的内容,指向的是文件的打开方式。 但是文件里面的内容并不会被覆盖!!并不会因为重定向,使得文件里的内容消失。
缓冲区
首先,来看一组代码
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
此时输出的结果为:
但如果我们执行程序时,进行输出重定向的话
那结果就变成了
我们可以发现printf和fwrite被打印了两次,这是为什么呢?
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
看上面代码,printf和fwrite是向文件中进行打印和写入的操作,所以是全缓冲,全缓冲的话,如果没有将缓冲区写满的话,就不会刷新缓冲区,那么也就没有及时打印出结果,后面进行fork()之后,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。父子进程在结束进程时,都会刷新对应的缓冲区,也就是缓冲区内的printf,fwrite会被刷新两次,当然也就会被打印出来两次。 而write是向显示器写入,向显示器写入是行缓冲,所以会直接刷新缓冲区,打印出来结果。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
理解文件系统
我们的文件如果已经被打开,会放到内存当中,但那些更多是没被打开的文件又会放到哪里呢? 实际上,没有被打开的文件会放在磁盘当中。
首先,我们从磁盘的角度来切入。
磁盘的物理结构
磁盘的逻辑结构
思考一个问题:
我们如何找到一个指定的扇区?
实际上就是先找到磁头,找到磁头之后就确定盘面了,我们接着要找磁道,最后再找到指定扇区就可以了。
回到最初的问题上,实际未被打开的文件,大体上说是储存在磁盘上,往细了讲就是分配在磁盘对应的一段扇区上。
接着我们对磁盘进行逻辑抽象就靠近到我们的文件系统了。
对磁盘进行逻辑抽象
我们可以把磁盘抽象成一大块数组,将数组划分成一个个的块。 拟出来的结果就是,一个数组中的一块就可以充当一个磁道,一个磁道又被划分成一个个的扇区。
文件系统inode
接着,我们来对这个磁盘文件系统图进行详细描述:
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子。
GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等。
数据区:存放文件内容。
inode是按分区来划分的,也就是说我们在一个分区中,inode编号是连续在一起的,从0开始。在另一个分区里,又是从0开始的,不同分区的inode号出现重复是正常的情况。
但是,我们要注意inode table表中存放的属性里,并没有文件名。 我们刚说了一个文件名对应一个inode,但是这个表里面没有文件名,那也就找不到对应的inode号,那inode号究竟是怎么来的呢?
目录中存放了inode和文件名的映射?
在Linux中,一切皆文件。 目录也不例外,目录 = 目录的属性+目录的内容。 在目录的内容中,就存放了目录下的文件名与inode的映射。
这也就是为什么,我们在访问到一个文件时,一定要加入相对路径或者绝对路径的原因。
我们在调用一个文件时,该文件会通过路径去往前查看目录,查看目录的过程又会不断往前继续找目录,直到溯源到根目录。 所以为了提高效率,Linux对路径会进行缓存,这样就可以提高访问效率了。
软硬链接
软链接:软链接类似于一个文件的别名,对文件进行软链接,会生成一个新的inode号。
我们可以想象一下windows下的软件的快捷方式,我们删除快捷方式时,软件并不会被真正删除。 所以我们对软链接进行删除,文件并没有被真的删除,我们对文件进行删除,那么软链接也就会失效。
硬链接:硬链接实际就是在指定目录下建立一个新的文件名和inode的映射关系。
硬链接一个文件时,不会产生一个新的inode号,而是会让硬链接数+1。
我们删除一个文件或者删除一个硬链接时,并不一定会把文件给删除。
如果一个文件被硬链接了,那么删除一个文件的方式是要让文件的硬链接数变为0。 我们删除文件,或者删除硬链接都只会让硬链接数-1,如果硬链接数没到0,就还没有真正删除这个文件。
软链接的作用:创建快捷方式,更加遍历
硬链接的作用:进行备份
静态库和动态库
静态库:程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。静态库以.a结尾。
动态库:程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。动态库以.so结尾。
各自的优点:
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。
静态库内的代码在编译时就已经链接到可执行文件中了,因此程序后续运行时,不需要去找库了,因此可以提高运行效率。
文件默认会去使用动态库,如果只有静态库,才会去调用静态库。
如果使用-static,那么就会强制执行静态库,此时如果没有静态库,或者只有动态库的话,不仅不会去使用动态库,还会因为没有静态库而报错!
使用动静态库时,链接的时候有两个参数需要注意:
l:链接动态库,只要库名即可(去掉lib以及版本号)。
L:链接库所在的路径。
接着,我们来看一看动态库是怎么调用实现的?
动态库调用实现的原理
可执行程序在编译时,会变成很多汇编语句,也就是说实际上那些函数名并不重要,因为编译之后都会变成一些地址。
一个进程加载到内存中,会把文件内容和动态库地址传入内存中,如果可执行程序形成task_struct里有地址空间,地址空间对应的页表中会有一个对应库的虚拟地址,页表到内存中的映射如果能形成,就会有对应的物理地址,指向内存中的数据。
可执行程序在加入到内存中,就会把动态库的地址加载到共享区。(库也要先描述再组织,加载到共享区中就相当于对库进行描述)。 先初始化地址空间(先描述),再把磁盘中的数据写入内存中(后组织)。(也可以理解成,一个准大学生入学时,先要将档案给到大学,之后人再过去报道具体的信息)
整个过程就是: 1.进程创建阶段,初始化地址空间,让CPU知道main函数入口的地址 2.加载:可执行程序编译后,每一行代码和数据就有了物理地址,自己的虚拟地址也有,两者就可以在页表中进行映射了。