文件的本质:进程和被打开文件的关系
C语言有文件操作接口,其他语言同样也有,并且都不一样
文件保存在磁盘上,磁盘属于硬件,只能通过操作系统进行访问;想要访问文件就绕不开操作系统;操作系统会提供一系列的系统调用接口进行文件访问
但是,操作系统只有一个,语言却存在很多种;所以无论语言是怎么变化的,系统调用的底层是不变化的
int fprintf(FILE *stream, const char *format, ...);
先介绍打印函数 fprintf
,将内容写到输入流中也就是写到文件中
结果如下
char *fgets(char *s, int size, FILE *stream);
介绍读取函数 fgets
在文件中读取大小为 size
的内容到字符串 s
中
结果如下
文件存在时:
int open(const char *pathname, int flags);
文件不存在时:
int open(const char *pathname, int flags, mode_t mode);
返回值
open() and creat() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
pathname
:文件名/文件路径flags
:标识符:O_RDONLY
只读, O_WRONLY
只写,O_CREAT
创建,O_APPEND
追加,O_TRUNC
每次写时把之前的内容清空;宏所定义的;每个宏对应的数值,只有一个比特位,彼此位置不重叠mode
:文件权限由于文件不存在,所以需要先创建O_CREAT
;文件权限默认是0666
结果如下
由于掩码的存在,文件权限并不是0666
ssize_t write(int fd, const void *buf, size_t count);
将大小为 count
的内容 buf
写到文件中;注意这里的内容类型没有限制,无论是二进制类还是文本类,在操作系统看来都是二进制类;写入成功时返回写入的大小 ssize_t
;注意在写入字符串时,这里不同于C语言的规定,不需要计算最后一个字符
结果如下
相比于库函数调用的写文件 w
就相当于系统调用FILE_NAME,O_WRONLY|O_CREAT|O_TRUNC,0666
;所以库函数调用本质是对系统调用的封装,追加,读取类似
在上面已经说明文件操作的本质是进程和被打开文件的关系,接下来进行深入学习
首先一个进程可以打开多个文件,所以系统中就必然存在着大量被打开的文件,系统便需要对这些文件进行管理,管理方式:先描述再组织;为了管理这些文件,创建对应的内核结构体struct file{ }
,其中包含文件的大部分属性
在上面系统调用打开文件时,返回的文件描述符是数字3,为什么呢???
再观察下列代码
同时打开三个文件,返回的文件描述符是连续的额3,4,5;为什么是从3开始呢,而且还是连续的呢???
其原因是,每个文件中都存在着三个标准输出流
stdin 键盘
stdout 显示器
stderr 显示器
它们占据着文件描述符的前三个位置
验证结果如下
在系统调用中接受返回值(文件描述符)是使用 int
;为什么在标准输出中也会存在着库函数调用接口中接受返回值的 FILE*
呢???
其实 FILE
本身是一个结构体,是对系统调用的封装,所以其中就有一部分是文件描述符
上面还剩一个问题没有解决,就是问什么为返回的文件描述符都是连续的呢???
下面进行图解
磁盘中的文件test.txt
加载到内存中,系统创建结构体struct file
对其进行管理;进程的结构体中存在着一个指向管理所有内存中文件的结构体,次结构体中存在这一个数组,也称进程的文件描述符表从下标为零的位置开始指向标准输出,输入,错误,紧接着就是管理文件的结构体;所以,文件描述符的本质就是数组的下标
观察下列代码,当我们把文件描述符fd==0
关闭时,结果会怎么样呢
如果关闭的是文件描述符fd==2
(不是关闭fd==1
,原因下面会解释,结果又是怎么样的呢?
由此便可以得出结论:文件描述符的分配规则是从小到大,按照顺序寻找最小的且没有被占用的文件描述符
很奇怪!!!为什么是这个结果,不应该是打印数字1吗?
文件描述符fd==1
是标准输出,也就是将结果打印到显示器上面,现在它被关闭了结果便是显示器并没有打印结果,很合理;那么本来应该被打印的结果去了哪里呢
下面进行图解
从图中可以看到,根据文件描述符的分配规则。当fd==1
被关闭之后,管理文件的结构体就寻找到该描述符,并将其指向自己;所以原本应该打印到显示器中内容,现在都被打印到了文件中,接下来打开文件进行验证
结果正确,其实这里还存在着缓冲区的问题,在后面会进行学习的;本应该打印到显示器的内容通过关闭描述符,就可以改变其打印的方向,这种操作就称作重定向,下面我们就来学习有关重定向的内容吧
重定向的本质:上层调用不变的情况下,改变底层的输出方向
上面是通过关闭文件描述符的方式进行重定向,这种方式太低端;这里介绍一种新的凡是进行重定向
int dup2(int oldfd, int newfd);
将文件描述符表中指向旧文件结构体的指针改变方向,指向新的文件结构体中;若成功,返回新的文件描述符
图解
重定向追加,标准输入类似,这里就不加赘述
Linux下一切皆文件,这句话该如何理解呢?
在冯诺依曼体系中,操作系统是通过驱动进而控制硬件;每种硬件其实都是由相应的结构体进行控制的,包括其读写功能;上面学习的当文件加载到内存中时会被其对应的结构体进行管理,其实对应的结构体中包含了两个重要的属性读写函数指针;当我们向键盘输入内容时,相应的文件就会调用其函数指针指向键盘结构体对应的功能,由此便可对上面的文件结构体进行补充:每个文件打开时,都会有对应的结构体去进行管理它,同时会调用函数指针完成初始化,并且找到该文件在内核中的缓冲区;所以就在文件结构体的上层来看,所有的设备和文件,统一都是通过调用文件结构体去完成,便可以解释Linux下一切皆文件
图解如下
由结果来看,直接打印到显示器上面时,代码正常;当重定向到文件中时,却有些不同,库函数调用接口都打印了两次,系统调用接口却只打印了一次,这又是为什么呢?
为了解决这个问题,先来学习缓存区的概念
缓冲区本质就是一段内存
如果要现在的快递行业极大地节省了我们的时间;如果要寄快递,只需要将快递放到寄存地点,物流就会帮我们将其送达目的地
其实物流同样也存在于操作系统中;比如我们向文件中写东西,大的角度来看是从内存将内容写到磁盘中,其底层是进程将数据写到文件中,其过程是:先将数据拷贝到缓冲区,缓冲区再将数据拷贝到文件中,缓冲区的存在极大地减少了数据写到文件的时间;这也与缓冲区的结构有关
缓冲区刷新的策略
如果存在一块数据,可以一次性全部都写入到外设中;也可以多次批量写入到外设中;缓冲区会根据具体的外设,制定相应的刷新策略
其实还存在两种刷新方式:用户强制刷新;进程退出时都会进行缓冲区刷新
缓冲区到底在哪呢?上面代码的打印结果与缓冲区有关,从结果来看,其一定不存在于内核之中;上面学习的三个标准输出流,输入流,错误流其类型是FILE*
,对应的结构体FILE
包含着文件描述符,并且还包括缓冲区
现在来解释上面打印结果差异的原因:打印到显示器上面时,stdout
默认是行刷新,在创建子进程之前,三条库函数调用已经将数据打印到显示器上,结构体FILE
中已经不存在对应的数据;重定向到文件中时,缓冲区采用的是全缓冲,库函数调用接口,虽然含有\n
,但是不足以将stdout
缓冲区写满,数据并没有被刷新,创建子进程时,stdout
属于父进程父进程,接着就是进程退出,父进程或子进程退出时,一定要进行缓冲区刷新,也就是进行数据修改,所有发生了写时拷贝,后一个进程退出时也会打印一次;系统调用之所以没有没打印两次,是因为write
中并没有FILE
结构体,所有也没有所谓的缓冲区
mystdio.h
1 #pragma once
2
3 #include<assert.h>
4 #include<stdlib.h>
5 #include<errno.h>
6 #include<string.h>
7 #include<unistd.h>
8 #include<sys/types.h>
9 #include<sys/stat.h>
10 #include<fcntl.h>
11
12 #define SIZE 1024
13 #define SYNC_NOW 1
14 #define SYNC_LINE 2
15 #define SYNC_FULL 4
16
17 typedef struct _FILE{
18 int flags;//刷新方式
19 int fileno;//文件描述符
20 int cap;//总容量
21 int size;//当前大小
22 char buffer[SIZE];
23 }FILE_;
24
25 FILE_ *fopen_(const char*path_name,const char*mode);
26 void fwrite_(const void*ptr,int num,FILE_ *fp);
27 void fclose_(FILE_*fp);
28 void fflush_(FILE_*fp);
mystdio.c
1 #include"mystdio.h"
2
3 FILE_*fopen_(const char*path_name,const char*mode)
4 {
5 int flags=0;
6 int defaultmode=0666;
7 if(strcmp(mode,"r")==0)
8 {
9 flags|=O_RDONLY;
10 }
11 else if(strcmp(mode,"w")==0)
12 {
13 flags|=(O_WRONLY|O_CREAT|O_TRUNC);
14 }
15 else if(strcmp(mode,"a")==0)
16 {
17 flags|=(O_WRONLY|O_CREAT|O_APPEND);
18 }
19 else
20 {
21
22 }
23
24 int fd=0;
25 if(flags&O_RDONLY)
26 fd=open(path_name,flags);
27 else
28 fd=open(path_name,defaultmode);
29
30 if(fd<0)
31 {
32 const char*err=strerror(errno);
33 write(2,err,strlen(err));
34 return NULL;
35 }
36
37 FILE_*fp=(FILE_*)malloc(sizeof(FILE_));
38 fp->flags=SYNC_LINE;
39
40 fp->fileno=fd;
41
42 fp->cap=SIZE;
43
44 memset(fp->buffer,0,SIZE);
45
46 return fp;
47 }
48
49 void fwrite_(const void*ptr,int num,FILE_*fp)
50 {
51 //写入到缓冲区
52 memcpy(fp->buffer+fp->size,ptr,num);
53 fp->size+=num;
54
55 //判断是否需要刷新
56 if(fp->flags&SYNC_NOW)
57 {
58 write(fp->fileno,fp->buffer,fp->size);
59 fp->size=0;//清空缓冲区
60 }
61 else if(fp->flags&SYNC_FULL)
62 {
63 if(fp->size==fp->cap)
64 {
65 write(fp->fileno,fp->buffer,fp->size);
66 fp->size=0;
67 }
68 }
69 else if(fp->flags&SYNC_LINE)
70 {
71 if(fp->buffer[fp->size-1]=='\n')
72 {
73 write(fp->fileno,fp->buffer,fp->size);
74 fp->size=0;
75 }
76 }
77 else
78 {
79
80 }
81 }
82
83
84 void fflush_(FILE_*fp)
85 {
86 if(fp->size>0)
87 write(fp->fileno,fp->buffer,fp->size);
88
89 fsync(fp->fileno);//强制要求刷新
90 fp->size=0;
91 }
92
93 void fclose_(FILE_*fp)
94 {
95 fflush_(fp);
96 close(fp->fileno);
97 }
当我们向文件中写入时,肯定不是直接将内容写入到文件中,其中还包含着许多步骤;首先将内容拷贝到库所提供的缓冲区中fwrite()
,也就是FILE
中的缓冲区,紧接着文件结构体struct_file
通过调用其函数指针将内容拷贝到内核缓冲区write()
;最后由操作系统决定按照什么样的刷新策略将内容写到文件中
如果一个文件没有被打开呢?磁盘中存在着许多没有被打开的文件,这些文件又该如何进行管理呢?
文件系统就是为了管理这些没有被打开的文件,在学习文件系统之前,先来了解磁盘的结构
磁盘是计算机中唯一的一个机械结构,同时也是外设
结构包括盘面和磁头,盘面的两面都有磁头;盘面通过马达控制旋转,磁头也通过马达控制左右摇摆,两者之间是没有任何接触的
磁盘被划分为多个同心圆,每个同心圆称作磁道也称作柱面,同时每个磁道又被分为多个圆弧,每个圆弧称作扇区,数据就存储在每个扇区中,并且每个扇区存储的大小都是512 byte
在盘面上进行寻址时,磁头来回摆动确定在哪个磁道上,紧接着盘面旋转再确定在哪个扇区上,这种方法称为 CHS定位法
磁盘物理结构上是圆盘形状,可以将其想象成线性结构,就像磁带一样,卷起来是圆形的,扯出来就是线性结构的
把磁盘从逻辑上看作是一个数组sector arr[n+1]
,每个元素是一个扇区,对磁盘的管理,转化为对数组进行管理;转化为逻辑结构之后,再进行寻址就变得简单很多,只需要知道这个扇区的下标就能定位到该扇区,在操作系统中,称这种地址为LBA
地址
优点:
进一步理解磁盘读写数据
虽然磁盘每次访问的基本单位都是512byte
,但相对来说还是太小,如果文件的大小是4字节或8字节,那么要访问磁盘8次或者16次,效率太低;所以操作系统每次访问时,会进行多个扇区的读写,以4字节作为基本单位
文件系统为了对磁盘,进行管理,先进行分区,就相当于将每个同心圆分隔出来;再进行分组,每个同心圆中有不同的区域,每个区域所存储的内容也不同
文件=内容+属性
Linux
的文件属性和文件内容是分批存储的;inode
是用来存储文件的几乎所有属性,出来文件名,每个文件对应一个inode
,大小固定,inode
为了进行彼此区分,都有自己的ID
;data block
存储着文件内容,随着应用类型的大小在变化
Super Block
:保存的是整个文件系统的信息,并不是所有块组中都有,主要是为了备份Group Descriptor Table
:对应分组的宏观属性信息Block Bitmap
:数据块对应的位图,位图中的比特位位置和当前data block
对应的数据块位置是一一对应的inode Bitmap
:inode
对应的位图结构;位图中比特位位置和当前文件对应的inode
的位置是一一对应的inode table
:保存了分组内部所有的inode
(已经使用或未被使用的),如果先要添加一个文件,首先要到该组中找到一个未被使用的inode
进行文件属性的存储data blocks
:保存分组内部所有文件的数据块当我们查找某个文件时,统一使用的是:inode
编号,先到inode Bitmap
中查看该文件是否存在,如果存在,可以读取文件的属性;如果要读取文件的内容又该如何呢?
inode
的数据结构中存储着自身的编号,大小,还有数组,当读取文件内容时,可通过该数组到data Blocks
中找到对应的扇区,便可对文件内容进行读写操作
文件删除操作就较为简单,只需要修改Block Bitmap
和inode Bitmap
即可
所有的文件都放在目录中,目录也是文件,也有自己的属性和内容,在Linux
的操作中,对文件或目录从来没有使用过inode
,而且文件名也不存储在inode
中,所以目录中的数据块存储的是什么呢???
其实目录中的数据块存储的是当前目录下的文件名于inode
的映射关系,这也就是解释了为什么在目录中新增文件必须有写入权限,因为新增文件时,需要在目录中写入文件名与inode
的映射关系
观察上面所创建的软硬链接,可以发现:软链接具有独立的 inode
可以作为独立文件,而硬链接却是和文件共用一个 inode
,既然如此,那么创建硬链接有何用处呢???
首先创建硬链接,根本没有创建新文件,所使用的还是原本文件的内容和 inode
,所以创建硬链接的目的就是在指定路径下,新增文件名和 inode
编号的映射关系
图解如下
inode
编号结构中存在着引用计数,记录着硬链接数,也就可以解释图中,为什么文件和硬链接的计数为2,因为两者全都指向了同一个 inode
编号;所以只有当一个文件的硬链接变为0时,这个文件才真正被删除
硬链接的作用如此,那软链接呢???
如果将文件删除,结果会怎样呢?
从图中可以看到,软链接此时已经出问题,而硬链接正常;如果再重新创建一个新文件会发生什么?
由此可见,软链接是只认识文件名,在系统中只能通过特定路径进行查找文件,就类似快捷方式,方便用户使用
观察下列指令
创建一个文件,硬链接数为1,因为其本身的文件名和自己的inode
具有映射关系,但是空的目录为什么硬链接数为2呢??
进入目录中发现,里面还存在着一个名为.
的隐藏文件,而且它的inode
编号还是和目录的inode
的编号一样,这就可以解释为什么一个目录中的硬链接数为2;不过,还可以目录中还存在着一个名为..
的文件,它的作用是什么呢???接下来慢慢揭晓
在目录yjm
中创建一个空目录dir
图中可以发现,创建完新目录后,原本目录中的硬链接就变成了3,进入新目录中,发现文件名为..
的文件inode
编号与原目录的inode
一样,所以这也就解释为什么硬链接数变化的原因;目录中文件名为.
的文件表示当前文件,文件名为..
的文件表示上级文件
为什么不允许给目录进行硬链接,却可以进行软链接?
原因很简单,如果给根目录进行硬链接,然后通过根目录寻找次链接,便会无限循环,出现问题
(.a)
:程序在编译链接时把库的代码链接到可执行文件中,程序运行时不再需要静态库(.so)
:程序在运行时才去链接动态库的代码,多个程序共享使用库的代码先完成一个小任务,在一个目录中完成两个函数的实现,然后将其二进制可执行文件.o
还有头文件.h
拷贝到另一个目录中,使用它自己的main
函数来完成最后的链接执行这个两个函数
加减函数的实现和头文件如下
将所有 .o
文件和头文件.h
拷贝到另一个目录中
main.c
所以如果我们不想给对方自己的源代码,只需要提供对方 .o
方法的实现, .h
包含的方法,对方便可使用自己的代码进行链接最后执行
如果有很多方法,那就需要拷贝多份 .o
文件,是不是很麻烦呢???所以想了个办法,将所有的 .o
文件进行打包,只提供对方一个文件,而这个文件也称为库,库又分为静态库和动态库,接下来就来学习吧
先介绍两个指令
ar
:是归档指令, archive
将文件打包到一个文件中
rc
:表示 replace and create
这只是将库文件进行了打包,在上面的例子中,还包括了头文件,所以还需要进行改进
output
:称作发布版本,打包之后便可进行发布
自己生成的静态库便完成了,只需要进行发布即可
在测试目录中进行库的下载(也就是拷贝),解压
在进行链接时,Linux
有些不同,在之前的学习中,链接阶段都是现在当前程序中寻找头文件,然后再到其他程序中寻找头文件;还需要告知编译器,当前库所在路径,不经如此还要告知其库的名称,当然在之前的学习中这一切都是编译器默认所做到
所以,链接操作如下
-I ./mylib/include
:告知编译器所包含的头文件
-L ./mylib/lib
:指明库所在路径
-l math
:指明库名称
为什么 mymath
是动态链接的,并且编译器所罗列的库中并没有刚刚自己所写的库,这是为什么呢???
gcc
默认是动态链接的,如果只有一个库,最后是动态链接还是静态链接却决于这个库的类型;但是如果存在很多库,当链接时如果使用了一个动态库,则就是动态链接,上面在最后链接时,库只提供了静态库,而默认是动态链接,所以只能将将代码拷贝到可执行程序中使用动态库进行链接
与静态库类似,有一点区别
在生成.o
文件时,需要加上fPIC
生成位置无关码
进行文件打包,这里可以直接使用gcc
再加上shared
生成共享库格式即可
这里与静态库有所不同,动态库已经加载到程序中,但是并没有找到;在链接时,我们已经将库文件,路径和库名称都告知了gcc
,当程序链接之后,与编译器就无关了;运行时操作系统和命令行解释器也是需要知道库所在的位置,但是由于库并没有在系统的默认路径下,所以操作系统无法找到,程序也就无法进行
解决措施也很简单,只需要将库路径添加到默认搜索路径即可,在当前目录中进行软链接,程序即可运行
静态区并不是加载到内存中的,在程序运行时所需要使用的函数被拷贝到程序的代码段中,且必须按照相对确定的地址进行编址,称作绝对编址;再由程序拷贝到内存中,再通过虚拟地址空间映射到代码段上进行访问
图解如下
动态库的加载是相对位置的加载,在程序中存在着库中函数的偏移量,这也解释了为什么上面使用 fPIC
生成位置无关码;程序加载到内存中的代码段,通过页表映射到虚拟地址空间的代码段中,当程序执行到库函数时,此时程序中只有函数的偏移量,进程便停止,将动态库加载到内存中,再通过页表映射到虚拟地址空间中的共享区,一旦库函数加载到共享区中,起始位置就确定了,然后程序通过偏移量在库中寻找对应的函数,至此动态库加载的内容结束