基础IO
- 回顾文件
- 回顾文件操作
- 库函数调用接口
- 写文件-w
- 读文件-r
- 系统调用
- 打开文件-open
- 写文件-write
- 文件操作本质
- 文件描述符fd
- 文件描述符的分配规则
- 重定向
- FILE
- 缓冲区
- 模拟实现缓冲区
- 总结
- 理解文件系统
- 磁盘
- 物理结构
- 存储结构
- 逻辑结构
- 文件系统
- 软硬链接
- 动静态库
- 动态库和静态库
- 生成静态库
- 生成动态库
- 动静态库的加载
- 静态库
- 动态库
回顾文件
- 空文件,也是要在磁盘中占据空间
- 文件=内容+属性
- 文件操作=内容+属性操作
- 标识一个文件:文件名+路径
- 一个文件要被访问必须先打开:先是通过进程调用接口,接着由操作系统去实现
- 磁盘中存在着打开的文件和未被打开的文件
文件的本质:进程和被打开文件的关系
回顾文件操作
C语言有文件操作接口,其他语言同样也有,并且都不一样
文件保存在磁盘上,磁盘属于硬件,只能通过操作系统进行访问;想要访问文件就绕不开操作系统;操作系统会提供一系列的系统调用接口进行文件访问
但是,操作系统只有一个,语言却存在很多种;所以无论语言是怎么变化的,系统调用的底层是不变化的
库函数调用接口
写文件-w
int fprintf(FILE *stream, const char *format, ...);
先介绍打印函数 fprintf
,将内容写到输入流中也就是写到文件中
结果如下
结果如下
读文件-r
char *fgets(char *s, int size, FILE *stream);
介绍读取函数 fgets
在文件中读取大小为 size
的内容到字符串 s
中
结果如下
系统调用
打开文件-open
文件存在时:
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
:文件权限- 返回值:成功,返回文件描述符;失败,返回-1
由于文件不存在,所以需要先创建O_CREAT
;文件权限默认是0666
结果如下
由于掩码的存在,文件权限并不是0666
写文件-write
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
本身是一个结构体,是对系统调用的封装,所以其中就有一部分是文件描述符
文件描述符fd
上面还剩一个问题没有解决,就是问什么为返回的文件描述符都是连续的呢???
下面进行图解
磁盘中的文件test.txt
加载到内存中,系统创建结构体struct file
对其进行管理;进程的结构体中存在着一个指向管理所有内存中文件的结构体,次结构体中存在这一个数组,也称进程的文件描述符表从下标为零的位置开始指向标准输出,输入,错误,紧接着就是管理文件的结构体;所以,文件描述符的本质就是数组的下标
文件描述符的分配规则
观察下列代码,当我们把文件描述符fd==0
关闭时,结果会怎么样呢
如果关闭的是文件描述符fd==2
(不是关闭fd==1
,原因下面会解释,结果又是怎么样的呢?
由此便可以得出结论:文件描述符的分配规则是从小到大,按照顺序寻找最小的且没有被占用的文件描述符
如果关闭文件描述符fd==1
,结果会怎样呢?
很奇怪!!!为什么是这个结果,不应该是打印数字1吗?
文件描述符fd==1
是标准输出,也就是将结果打印到显示器上面,现在它被关闭了结果便是显示器并没有打印结果,很合理;那么本来应该被打印的结果去了哪里呢
下面进行图解
从图中可以看到,根据文件描述符的分配规则。当fd==1
被关闭之后,管理文件的结构体就寻找到该描述符,并将其指向自己;所以原本应该打印到显示器中内容,现在都被打印到了文件中,接下来打开文件进行验证
结果正确,其实这里还存在着缓冲区的问题,在后面会进行学习的;本应该打印到显示器的内容通过关闭描述符,就可以改变其打印的方向,这种操作就称作重定向,下面我们就来学习有关重定向的内容吧
重定向
重定向的本质:上层调用不变的情况下,改变底层的输出方向
上面是通过关闭文件描述符的方式进行重定向,这种方式太低端;这里介绍一种新的凡是进行重定向
int dup2(int oldfd, int newfd);
将文件描述符表中指向旧文件结构体的指针改变方向,指向新的文件结构体中;若成功,返回新的文件描述符
重定向标准输出dup2(fd,2)
代码如下
图解
重定向追加,标准输入类似,这里就不加赘述
Linux下一切皆文件,这句话该如何理解呢?
在冯诺依曼体系中,操作系统是通过驱动进而控制硬件;每种硬件其实都是由相应的结构体进行控制的,包括其读写功能;上面学习的当文件加载到内存中时会被其对应的结构体进行管理,其实对应的结构体中包含了两个重要的属性读写函数指针;当我们向键盘输入内容时,相应的文件就会调用其函数指针指向键盘结构体对应的功能,由此便可对上面的文件结构体进行补充:每个文件打开时,都会有对应的结构体去进行管理它,同时会调用函数指针完成初始化,并且找到该文件在内核中的缓冲区;所以就在文件结构体的上层来看,所有的设备和文件,统一都是通过调用文件结构体去完成,便可以解释Linux下一切皆文件
图解如下
FILE
观察下列代码
由结果来看,直接打印到显示器上面时,代码正常;当重定向到文件中时,却有些不同,库函数调用接口都打印了两次,系统调用接口却只打印了一次,这又是为什么呢?
为了解决这个问题,先来学习缓存区的概念
缓冲区
缓冲区本质就是一段内存
如果要现在的快递行业极大地节省了我们的时间;如果要寄快递,只需要将快递放到寄存地点,物流就会帮我们将其送达目的地
其实物流同样也存在于操作系统中;比如我们向文件中写东西,大的角度来看是从内存将内容写到磁盘中,其底层是进程将数据写到文件中,其过程是:先将数据拷贝到缓冲区,缓冲区再将数据拷贝到文件中,缓冲区的存在极大地减少了数据写到文件的时间;这也与缓冲区的结构有关
缓冲区刷新的策略
如果存在一块数据,可以一次性全部都写入到外设中;也可以多次批量写入到外设中;缓冲区会根据具体的外设,制定相应的刷新策略
- 立即刷新也就是无缓冲
- 行刷新-行缓冲对应的外设就是显示器
- 当缓冲区已经满时进行刷新-全缓冲
其实还存在两种刷新方式:用户强制刷新;进程退出时都会进行缓冲区刷新
缓冲区到底在哪呢?上面代码的打印结果与缓冲区有关,从结果来看,其一定不存在于内核之中;上面学习的三个标准输出流,输入流,错误流其类型是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
函数来完成最后的链接执行这个两个函数
加减函数的实现和头文件如下
通过 gcc -c
生成可执行二进制文件
将所有 .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
生成位置无关码;程序加载到内存中的代码段,通过页表映射到虚拟地址空间的代码段中,当程序执行到库函数时,此时程序中只有函数的偏移量,进程便停止,将动态库加载到内存中,再通过页表映射到虚拟地址空间中的共享区,一旦库函数加载到共享区中,起始位置就确定了,然后程序通过偏移量在库中寻找对应的函数,至此动态库加载的内容结束