我们在C语言中接触过文件操作,现在我们来回顾一下在我们C中的文件操作
写文件:
我们先创建了一个log.txt而后将A到Z输进了这个文件,最后关闭文件
我们可以看到,我们将26个字母放到了这个文件中
上面展示了我们的fopen操作,我们先来认识一下这个操作
FILE *fopen(const char *path, const char *mode);
这就是fopen函数,我们现在对其进行解释
path :要打开的文件
mode:打开方式
r: 以读方式打开,如果当前文件不存在,则会报错
r+: 以读写方式打开,如果当前打开文件不存在,则报错
w: 以写方式打开,如果文件不存在,则在当前目录下创建该文件;如果当前文件存在,则将当前文件截断(清空)
w+: 以读写方式打开,其他和w形式相同
a: 以追加方式打开(文件流指针指向当前文件的尾部,不能读),如果文件不存在则创建
a+: 以追加方式打开,如果文件不存在则创建,支持可读可写
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr: 将fread读到的内容保存在ptr下
size: 块的大小
nmemb: 需要读的块的个数 size* nmemb == 总的字节数量
stream: 文件流指针,从哪里读
返回值: 返回成功读取到的块的个数
用法: 将块的大小指定为1(1个字节),需要读的块的个数就可以认为是多少个字节(ptr的内存空间的大小);
int fclose(FILE *fq);
关闭文件
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:需要写的数据,写的数据保存在ptr数组中
size:写数据时块的大小
nmemb:要写入块的个数
stream:文件流指针,写到哪里去
用法:将size置1,然后返回值就是成功写入数据所占的字节数量
那么我们如果想将hello world打到显示器上呢?
这样我们就将hello world打到了显示器上,我们其实只是将刚刚输出文件的位置换成了我们的显示器stdout,就可以将内容打到显示器上,这其实正说明了我们的显示器其实也是文件,也印证了在Linux中的一切皆文件
上面这些都是我们C语言中的文件接口,下面我们还有系统级的接口
我们的系统调用接口其实就是语言的底层接口,语言层的库函数其实都是架在系统调用接口之上的服务于各个语言的接口
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
我们打开了5个文件,并输出其对应的文件描述符,但是我们看到编号是从3开始的,那么我们的0,1,2去哪里了呢?
其实我们的0,1,2分别对应了我们熟知的的0-标准输入,1-标准输出,2-标准错误而我们的这些文件描述符都是数组下标,我们的操作系统其实是使用了一个函数指针数组来对这繁多的文件进行管理的,而我们的文件描述符所对应的就是数组下标
当我们的一个进程要被执行时,进程先找到自己的PCB ,task_struct,而后我们调用函数对文件进行写入,我们就可以根据函数中的文件操作符(索引)去找所对应的文件,去找到方法
那么我们的struct_file又是怎么去对文件进行操作的呢
我们不同的struct_file中存的都是相同的函数指针,而这些函数指针则分别去指向不同的设备,设备中的方法构成了驱动代码层,我们的struct_file则构成了虚拟层,所以在我们上层操作系统看来,我们的不同设备,硬盘,显示器等都是struct_file,也就是我们的文件,所以这就是我们的一切皆文件,而我们的struct_file中指向硬盘,就是硬盘的方法,指向显示器,就是显示器的方法,也体现了我们面向对象中的多态
我们再来回顾一下上述过程,我们打开一个文件,open,操作系统底层其实就是给我们创建了一个文件,创建了一个struct_file结构体,而又因为是进程task_struck创建的文件,所以我们就需要将进程与文件关联起来,利用的就是我们的数组struct_file *fd_array[],操作系统遍历数组,找到最小的未被使用的元素,将新申请的文件地址填到数组中,而后open函数返回文件描述符,以供我们找到对应文件
而如果我们对系统默认的设备,如0,1,2进行关闭,再创建新的文件,我们就会发现,默认的系统设备被新文件替代了,这因为我们的文件描述符分配规则分配给了当前没有被使用的最小下标,且设备与我们的其他文件一样,都是可以被视为文件的,而我们使用其他文件去代替原来的文件的过程,就叫重定向
我们的重定向其实是有两种,普通重定向与追加重定向
我们使用>符号,将hello linux重定向到了log.txt
我们使用>>对其进行追加重定向,就将两段hello linux定向到了log.txt中
而我们重定向的本质,其实是数组中的指针,指向了新文件
int dup2(int oldfd, int newfd);
我们的newfd是oldfd的一份拷贝,其本质就是将数组中下标为oldfd的内容拷贝到newfd中
我们在之前重定向时,是需要将原先的文件关闭的,不过在我们的dup2中,不需要关闭原文件
FILE其实是一个结构体,作为类型使用,如FILE* stdin,FILE* stdout,FILE* stderr
我们来看一段代码进行更深层次的研究
结果是这样
之后如果我们进行输出重定向呢?
我们发现print与fwrite输出了两次,write输出了一次,这是为什么呢?怎么重定向还改变了文件的输出?
事实上,我们打印文件都是需要文件描述符来打开文件的,重定向不过是改变了数组对文件的指向
为什么C库函数输出两次,系统调用输出一次?
其实我们一开始将数据写入文件为行缓冲,当定向成普通文件时变为了全缓冲,而执行到fork之前数据都保存在缓冲区中,都是父进程的数据,fork之后,会对数据进行拷贝,而后刷新缓冲区将数据打印出来,而我们的write没有自己的缓冲区,就不会刷出来
一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。 printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。 而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后 但是进程退出之后,会统一刷新,写入文件当中。 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。 write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的 “ 上层 ” , 是对系统调用的“ 封装 ” ,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由 C 标准库提供。
我们在介绍inode之前需要先来简单介绍下文件系统
这就是我们的硬盘,磁盘中会有两个面,每个面都可以存储数据,读写的最小单位为扇区,一般大小为512个字节,而我们的扇区组成了逻辑块,也称数据块,在Linux中逻辑块大小为4kb,也就是8个扇区,我们在读取数据时需要先确定在那个磁盘面,每个磁盘面有一个磁头,磁头帮助我们在这个磁盘面中查找,而磁头确认好了之后还需要确认柱面,也就是同心圆圈,最后确定在哪个扇区,找到数据
而我们的操作系统在对硬盘进行管理时对其进行了分区
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck 和 inode 的总量,未使用的block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了 GDT , Group Descriptor Table :块组描述符,描述块组属性信息 块位图( Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用 inode 位图( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。 i 节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等 数据区:存放文件内容
我们可以看到,我们的inode,就存储在我们的块组中,那么我们的inode到底是什么呢?
inode其实就是文件属性的集合,与文件实现1对1的关系,一个文件只能有一个inode
而我们要找一个文件,就需要先找到inode,要找到inode,就需要先找到inode id
讲了这么多,那么我们到底是怎么通过inode去找到文件的呢?
因为我们的文件与inode是一对一的关系,所以我们如果想找到文件,只需要找到其inide即可,先找到其inode id,而后通过inode中的内置指针数组找到文件对应的数据块
我们可以使用ls -lia,多带一个i,来查询文件的inodeid
所以我们有两大块用来存储文件信息的空间:数据块空间,inode块空间,这两大空间分别由各自的位图来记录存储情况,所以我们其实可以想到,导致一个文件创建失败,其实是有两种原因,一种是数据块不足,一种其实是inode块不足
inode与数据块分别有各自的bitmap以位图填1的方式记录着
我们其实可以发现,一个文件的属性与数据其实是分开的,分别存储在不同的区域中
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过 touch 一个新文件来看看如何工作。[root@localhost linux]# touch abc [root@localhost linux]# ls -i abc 263466 abc
创建一个新文件主要有一下4个操作
1. 存储属性 内核先找到一个空闲的 i 节点(这里是 263466 )。内核把文件信息记录到其中。 2. 存储数据 该文件需要存储在三个磁盘块,内核找到了三个空闲块: 300,500 , 800 。将内核缓冲区的第一块数据复制到300 ,下一块复制到 500 ,以此类推。 3. 记录分配情况 文件内容按顺序 300,500,800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。 4. 添加文件名到目录 新的文件名 abc 。 linux 如何在当前的目录中记录这个文件?内核将入口( 263466 , abc )添加到目录文件。文件名和inode 之间的对应关系将文件名和文件的内容及属性连接起来
我们看到,真正找到磁盘上文件的并不是文件名,而是 inode 。 其实在 linux 中可以让多个文件名对应于同一个inode。
[root@localhost linux]# touch abc [root@localhost linux]# ln abc def [root@localhost linux]# ls -1i abc def 263466 abc 263466 def
abc 和 def 的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数, inode 263466 的硬连接数为 2 。 我们在删除文件时干了两件事情: 1. 在目录中将对应的记录删除, 2. 将硬连接数 -1 ,如果为 0 ,则将对应的磁盘释放。
我们先是创建了一个txt文件,而后用ln命令让1.txtHard与1.txt共享一个inode实现硬链接,我们发现其目录中增加了一个个数,由1变为了2,说明其一个inode对应了两个文件名,但是文件名却又不能代表文件,真正的文件还是inode所决定的,所以这就说明文件系统中实际只有一个文件,只是增加了一层给对应关系而已
我们将一个txt文件进行修改,发现另一个也随之修改了
我们对其中一个文件进行删除,对另一个文件也没有影响
我们的硬链接是由inode去引用另一个文件,而我们的软链接则是由文件名去引用另一个文件,这这其实就可以道出他们两个链接最大的区别,硬链接共享inode,软链接不共享inode,具有独立的inode,这说明软链接链接出的是一个独立的文件
实现软链接的方法:ln-s指向的文件 链接文件
我们实现了一个软链接,inode并不一样,其具有独立的inode,其实也就相当于我们在文件系统中增加了一个文件
我们设置了两个文件,一个是软链接文件test,另一个是链接对象链接的对象文件test1,我们对test进行了编写,但未对test1进行编写,但是运行出来结果是一样的,事实上是因为我们在读取软链接文件时实际读取的是连接的对象文件的路径
但是我们将链接文件删除,读取软连接文件时会进行报错文件,而我们软链接文件引用的是链接对象的文件名,并不是inode,所以链接数量不变
其实我们可以发现,我们的软连接很像我们window下的快捷方式,通过一个快捷方式直接打开文件,而并不需要深入到路径中去寻找,可以大大降低访问成本
1.软连接是一个单独的文件,具有独立的inode,实际上是在文件系统中增加了一个文件
2.硬链接不是一个独立的文件,实际是存在在目录的数据块中,增加链接文件的inode与文件名的对应关系
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一般默认生成的可执行程序都是动态的,动态库体积小,运行时加载,只有一份。
事实上,我们通过一个简单的例子粗略类比一下这两个库,假设我们出去旅游,需要带食物,静态库就相当于我们自己带我们所需要的食物,而动态库则是在途径的商店中购买食物,所以我们两个库的特点也很明显,静态库体量大,但是很方便,饿了就可以去直接吃东西,而动态库则需要我们饿了时跑去商店买,效率没有静态库高,但是体量很小,节约空间
静态库( .a ):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库 动态库( .so ):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
我们使用ldd命令查看了文件与库之间的依赖关系,库名为去掉lib与第一个点之后的所有东西,所以我们这个库我们的c库
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking) 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
我们的gcc一般默认的都是动态链接,如果想实现静态链接则需要在链接时后面加上-static,我们也可以使用file 命令来对文件进行查看是静态还是动态链接
注意:动态链接并不是将库中的所有代码都链接,而是将使用的代码进行链接
我们先对三个文件进行编写,并生成目标文件,而后再用目标文件生成静态库
并查看静态库中的目录列表
t:列出静态库的文件
v:verbose详细文件
而后运行得到结果
-fPIC:产生位置无关的
-shared:产生共享库
我们同样先用三个文件的代码
而后生成动态库
我们的动态库就生成完毕了
动态库的使用:l:连接动态库,只要库名即可;L:链接库所在路径
最后运行main.c得到结果
总结:
编写一个动态库:
获得源文件和头文件
将源文件生成目标文件,gcc -fPIC -o
目标文件生成静态库,gcc -share -o *.so *.o
动态库:就是所有头文件和库文件使用动态库:
编译时要让编译器知道库文件路径,头文件路径和库名
让系统知道库文件路径。添加环境变量。将库文件绝对路径加到LD_LIBRARY_PATH中。