1. 文件
1.1 基础概念
- 文件 = 文件内容 + 属性(也是数据)
- 对文件的所有操作,无外乎是:a.对内容 b.对属性
- 站在系统的角度,能够被input读取,或者能够output写出的设备就叫做文件
- 狭义文件:普通的磁盘文件
- 广义文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可以称之为文件
1.2 访问文件
- 访问文件的本质:是进程在访问
- 文件在磁盘(硬件)上放着,而向硬件写入,只有操作系统才有这个权利
- 如果普通用户,也想写入那就必须让OS提供接口----文件类的系统调用接口
1.3 关于系统接口
- 一旦使用系统接口,编写所谓的文件代码,就无法再其他平台上直接运行了,
- 而像C++/Java这样的语言,它们就对文件系统接口进行了封装,包括所有访问文件的操作
- C++对所有的平台的代码都实现一遍,通过条件编译,动态裁剪实现封装性
为什么要学习OS层的文件接口?
- 不同的语言,有不同的语言级别的文件访问接口(都是不一样的),但是底层都封装了系统接口
- 系统接口只有一个,OS也只有一个,
1.4 C语言文件接口
1.4.1写入接口: fwrite fprintf fputs
- 在fwrite写入时,传的strlen(s1),不需要加一
- \0结尾是C语言的规定,文件不用遵守,文件要保存的是有效数据!
1.4.2 读取接口: fgets
- 这里以只读的方式打开
1.5 系统文件接口
1.5.1 打开文件: open
- pathname表示文件名,
- flags是一种标记,标记打开文件的状态
- mode表示没有文件,创建文件时给的初始权限
open的第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
open的第二个参数
open函数的第二个参数是flags,表示打开文件的方式。
其中常用选项有如下几个
参数选项 | 含义 |
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY | O_CREAT
扩展: flags标记
- 系统中的 flags就是宏定义
open的第三个参数
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:
- -rw-rw-rw-
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
- -rw-rw-r--
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
注意: 当不需要创建文件时,open的第三个参数可以不必设置
open的返回值
open函数的返回值是新打开文件的文件描述符。
1.5.2 打开-读-写-关闭 : open write read close
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);// 清空过滤权限
//fopen("log.txt", "w");
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);// 写入 创建 清空
const char* s = "hello write\n";
//const char* s = "aa\n";
write(fd, s, strlen(s));
flose(fd);
//fopen("log.txt", "a");
int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);// 写入 创建 追加
printf("open success, fd: %d\n", fd1);
int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);// 写入 创建 追加
printf("open success, fd: %d\n", fd2);
int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);// 写入 创建 追加
printf("open success, fd: %d\n", fd3);
int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);// 写入 创建 追加
printf("open success, fd: %d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
//fopen("log.txt", "r");
int fdd = open("log.txt", O_RDONLY); // 只读
char buffer[64];
memset(buffer, '\0', sizeof(buffer));
read(fdd, buffer, sizeof(buffer));
printf("%s", buffer);
flose(fdd);
return 0;
}
- 我们在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能要做非常多的动作!
1.6 文件描述符-fd
在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件
这里借用一下上面程序的结果
- 这里怎么是从3开始的呢,0 1 2 去那哪里了?
- 一个文件打开默认会打开stdin stdout stderr(标准输入 标准输出 标准错误),它们也用0 1 2 表示
1.6.1 文件指针FILE*的认识
- FILE是一个struct结构体(内部有很多成员),是由C标准库提供的
- 而在C文件库函数内部一定要调用系统调用,站在系统角度,它只认识fd,所以可以推出FILE结构体里面必定封装了fd!!!
1.6.2 证明FILE内部封装了fd
1.6.2 fd的理解
fd在内核本质是一个数组下标
- 进程要访问文件,必须先打开文件
- 文件要被访问,前提是加载到内存中,才能被直接访问
- 一般而言 进程 : 打开的文件 = 1 : n,但是如果是多个进程都要打开自己的文件呢?
- 系统中会存在大量的被打开的文件,OS面对如此的文件也要管理起来,通过先描述,再组织
- 在内核中,OS内部会为了管理一个打开的文件,构建一个FILE结构体
- 创建的struct file的对象,充当一个被打开的文件,如果用的很多,就用双链表组织起来,
- 上面这张是进程和文件之间的关系
- fd在内核本质是一个数组下标
1.7 文件的分类
-
磁盘文件(没有被打开)
-
内存文件(被进程在内存中打开)
对一个内存文件的操作流程
1.8 理解输出重定向
每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
// 这里的fd的分配规则是: 最小的,没有被占用的文件描述符
// 0,1,2 -> close(1) -> fd -> 1
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd); // stdout->FILE{fileno=1}->log.txt
printf("fd: %d\n", fd);
fprintf(stdout, "hello fprintf\n");
const char* s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
fflush(stdout); //stdout->_fileno == 1;
close(fd); //后面解释
return 0;
}
- 不管是输出重定向还是输入重定向,都是在OS内部,更改fd对应的内容指向
1.8.1 更改fd->dup2
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
if (argc != 2) {
return 2;
}
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
fprintf(stdout, "%s\n", argv[1]); //stdout->1->显示器
return 0;
}
- dup2使old_fd变成new_fd,就相当于更改fd对应的内容指向
1.9 Linux下一切皆文件
linux是用C语言写的,而C语言要实现面向对象,甚至是运行时多态,必须封装类
但是C语言的struct只能封装成员属性,要封装成员方法,就需要使用函数指针(用来调用成员方法)
- linux的设计哲学:体现在操作系统的软件设计层面
- Linux看待所有硬件都是struct file,通过函数指针调来调去,这是一种统一化
2. 缓冲区
缓冲区就是一段内存空间
- 存在缓冲区的原因是:为了提高整机效率,提高用户的响应速度,
2.1 刷新策略
- 立即刷新
- 行刷新(行缓冲) \n
- 满刷新(全缓冲)
所有设备都是倾向于全缓冲的,特殊情况:a.用户强制刷新(fflush),b.进程退出
2.2 缓冲区的位置
- 第一个程序,向显示器打印输出4行文本,
- 向普通文件(磁盘上),打印的时候,变成了7行,其中:
- C语言 IO接口是打印了2次的
- 系统接口,只打印1次和向显示器打印的一样
- 第二个程序,加上了fflush(stdout)强制刷新,向显示器和向普通文件(磁盘上)输出的文本都是4行
解释上面2个程序产生不同结果的原因
- 如果向显示器打印,刷新策略就是行刷新,那么最后执行fork的时候,
一定是函数执行完了 && 数据已经被刷新了,那么fork就无意义 - 如果你对应的程序进行了重定向,要向磁盘文件打印
隐形的刷新策略就变成的了全缓冲,那么\n就无意义 - 第2个程序中的fflush(stdout)强制刷新,不管刷新策略是什么都会被强制刷新
- fork()之前的函数已经执行完了,但是并不代表数据已经刷新了!!
这些数据在当前进程对应的C标准库中的缓冲区中
这些数据也是父进程的数据,fork()创建子进程,是会发生写时拷贝的,这就是为第一个程序向普通文件(磁盘上),打印的时候,变成了7行的原因
缓冲区如果是OS统一提供的,那么我们上面的代码,表现应该是一样,所以是C标准库提供且维护的
2.3 用户级缓冲区
-
C标准库给我们提供的用户级缓冲区,在struct FILE 结构体中,这个结构体中不仅封装了fd
还包含了该文件fd对应的语言层的缓冲区结构
2.4 简单设计用户级缓冲区
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
#define NUM 1024
struct MyFILE_ {
int fd;
char buffer[1024];
int end; //当前缓冲区的结尾
};
typedef struct MyFILE_ MyFILE;
MyFILE* fopen_(const char* pathname, const char* mode)
{
assert(pathname);
assert(mode);
MyFILE* fp = NULL;
if (*mode == 'w') {
// 假设是以'w'的方式打开
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);// 系统接口
if (fd >= 0)
{
fp = (MyFILE*)malloc(sizeof(MyFILE));
memset(fp, 0, sizeof(MyFILE));
fp->fd = fd;
}
}
return fp;
}
// C标准库中的实现!
void fputs_(const char* message, MyFILE* fp)
{
assert(message);
assert(fp);
// 把数据存放在缓冲区中
strcpy(fp->buffer + fp->end, message); //abcde\0
fp->end += strlen(message);
printf("%s\n", fp->buffer);
//暂时没有刷新
if (fp->fd == 0)
{
//标准输入
}
else if (fp->fd == 1)
{
//标准输出
if (fp->buffer[fp->end - 1] == '\n')
{
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if (fp->fd == 2)
{
//标准错误
}
else
{
//其他文件
}
}
void fflush_(MyFILE* fp)
{
assert(fp);
if (fp->end != 0)
{
//暂且认为刷新了--其实是把数据写到了内核
write(fp->fd, fp->buffer, fp->end);
syncfs(fp->fd); //将数据写入到磁盘
fp->end = 0;
}
}
void fclose_(MyFILE* fp)
{
assert(fp);
fflush_(fp);
close(fp->fd);
free(fp);
}
int main()
{
//close(1);
MyFILE* fp = fopen_("./log.txt", "w");
if (fp == NULL)
{
printf("open file error");
return 1;
}
fputs_("one: hello world", fp);
fork();
fclose_(fp);
}
2.5 对stdout和stderr的补充
- 1和2对应的都是显示器文件
- ./myfile > ok.txt 2>err.txt 将标准输出重定向到ok.txt中,标准错误重定向到err.txt
- ./myfile > log.txt 2>&1 将标准输出和标准错误都重定向到log.txt
3. 文件系统
3.1 背景知识
- 文件系统中的磁盘->磁盘级文件是没有被打开的文件
- 学磁盘级别文件的侧重点
- 单个文件角度 -- 这个文件位置?,多大?,其他属性...
- 站在系统角度 -- 一共有多少个文件?,各自的属性在哪?,如何快速找到,..
- 磁盘文件
- 内存 - 掉电易失存储介质
- 磁盘 -- 永久性存储介质 -- SSD,U盘,flash卡,光盘,磁带
磁盘不仅是一个外设,而且还是我们计算机中唯一的一个机械设备
4. 磁盘结构
4.1 物理结构
- 磁盘的盘面上会存储数据,里面有大量的像磁铁一样的东西,它们是有正负性的
- 先磁盘里写入,本质就是改变磁盘上的正负性,毕竟计算机只认识二进制
4.2 存储结构
- 灰色的圆圈叫做磁道,磁道的一部分叫做扇区
4.2.1 CHS寻址方案
在物理上,如何把数据写入到指定的扇区中?
- 通过一中叫CHS的寻址方案,通过这种方案所有的扇区就都能够找到了
- 1.在那一个面上(对应的就是那一个磁头)
- 2.在那一个磁道(柱面)上
- 3.在那一个扇区上
4.2.2 磁盘的抽象(虚拟,逻辑)结构
- 磁盘的存储数据的基本单位是512字节
- 将磁盘的盘面展开,想象成一种线性结构
- 访问一个扇区,只要通过数组的下标就可以了
4.3 对磁盘的管理
- 将数据存储到磁盘中 -> 将数据存储到该数组
- 找到磁盘特定扇区的位置 -> 找到数组特定的位置
- 对磁盘的管理 - > 对数组的管理
对一个小分区(小数组) 的管理
- 虽然磁盘的基本单位是扇区(512字节),但是操作系统(文件系统)和磁盘进行IO的基本单位是:4KB(8*512byte),硬件和软件(OS)进行了解耦!
- 如果基本单位太小了,有可能会导致多次IO,进而导致效率的降低!
- 如果操作系统使用的是和磁盘一样的大小,万一磁盘基本大小变了的话,OS的源代码也必须发生改变,所以IO没有以512字节为基本单位
4.4 存储文件
在Linux在磁盘上存储文件的时候,内容和属性是分开存储的!
- Date blocks:多个4KB(扇区*8)大小的集合,报错的都是特定文件的内容
- inode Table:inode是一个大小为128字节的空间,保存的是对应文件的属性
该块组内,所有文件的inode空间的集合,需要标识唯一性,每一个inode块,都要有一个inode编号!
一般而言,一个文件,一个inode对应node编号 - inode Bitmap:假设有10000+个blocks,10000+比特位:比特位和特定的indoe是一一对应的,其中比特位为1,代表该inode被占用,否则表示可用
- Block Bitmap:假设有10000+个blocks,10000+比特位:比特位和特定的block是一一对应的,其中比特位为1,代表该block被占用,否则表示可用
- GDT(简写): 快组描述符,这个快组多大,已经使用了多少了,有多少个inode,已经占用了多少个,还剩多少,一共多少个block,使用了多少...
- SuperBlock(简写):文件系统的属性信息
- 这些分区使一个文件的信息可追溯,可管理
上面的快组分割成为上面的内容,并且写入相关的管理数据 -> 每一个快组都这么做 -> 整个分区就被写入了文件系统信息(格式化)
- 一个文件"只"对应一个inode属性节点,inode编号,但是一个文件不只有一个block,
4.4.1 认识inode编号
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息
- 在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号
- 也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
如何理解创建一个空文件?
- 通过遍历inode位图的方式,找到一个空闲的inode。
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中。
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
如何理解对文件写入信息?
- 通过文件的inode编号找到对应的inode结构。
- 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
- 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。
如何理解删除一个文件?
- 将该文件对应的inode在inode位图当中置为无效。
- 将该文件申请过的数据块在块位图当中置为无效。
注意: 短时间是可以恢复的,但如果后续创建其他文件时或者是对其他文件进行了写入操作
申请inode编号和数据块号,可能会将该置为无效的inode和数据块号分配出去,
此时删除文件的数据就会被覆盖,也就无法恢复文件了
为什么拷贝文件的时候很慢,而删除文件的时候很快?
- 因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,
- 之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,
- 而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
- 这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。
如何理解目录
- 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意:
- 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。
- 因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
4.4.2 文件名 VS inode编号
- 找到文件: inode编号(依托于目录结构) -> 分区特定的bg -> inode -> 属性 -> 内容
- 在linux中inode属性里面,是没有文件名这样的说法的,但是目录也是文件,它也应该有自己的inode,有自己的data block,
- 文件名 : inode 编号存在一种映射关系,互为Key值
- inode是固定的,datablock是固定的!
4.4.2 在linux中删除文件,能恢复出来吗?
- 能恢复出来,这与inode编号有关,如果inode编号,没有被使用,inode和datablock没有被重复占用,就可以恢复删除文件
4.4.3 文件的三个时间
- Access: 文件最后被访问的时间。
- Modify: 文件内容最后的修改时间。
- Change: 文件属性最后的修改时间。
- 当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,
- 但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。
注意: 当某一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。
5. 软硬连接
5.1 软连接: ln -s 源文件 目标文件
- 软连接有独立的inode -> 它是一个独立文件
- 特性: 软连接的文件内容,是指向的文件对应的路径!
- 应用: 相当于windows下的快捷方式
5.2 硬链接: ln 源文件 目标文件
- 硬连接没有独立的inode -> 它不是一个独立文件
- 创建硬链接时,仅仅只是在指定的目录下,建立了文件名 和 指定inode的映射关系
5.3 软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录
5.4 inode的引用计数
- 这个引用计数表示有多少个文件名与它相关,
- 当我们删除一个文件的时候,并不是把这个文件inode删除,而是将这个文件的inode引用计数--
当引用计数为0的时候,这个文件,才是真正的删除
- 默认创建目录:引用计数(硬链接)默认是2
- 自己目录名:inode
- 自己目录内部: inode
6. 动态库.so 与 静态库.a
6.1 编写: 静态库
- 生成一个静态库的命令: ar -rc lib文件名.a 其他.o文件
- hello这个包里面有.h和静态库
6.2 使用: 静态库
- 第一步就是要把hello这个包拷贝到当前路径,也就是间接性的把这个静态库拷贝当前路径
- gcc 源文件 -I(大写的i) 头文件搜索路径 -L 库文件搜索路径 -l (小写的l)指定静态库
6.3 编写: 动态库
-
gcc -c -fPIC 其他.c -o 其他.o
-
gcc -shared 其他.o -o lib文件名.so
6.4 使用: 动态库
- 动态库是一个独立的库文件,它可以和可执行程序,分批加载!!!
- 当动态库和静态库同时存在时,默认调用动态库
- 如果这时要使用静态库,需要在最后面加上-static
上面的那段代码只是给gcc说了动态库路径,系统加载器不知道,需要给它说明动态库路径
6.4.1 方法一: 环境变量
- export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:要使用的动态库路径
- 不过这种方式重启之后就会被清除
6.4.2 方法二: 新增配置文件
- sudo touch /etc/ld.so.conf.d/文件名.conf
- sudo vim /etc/ld.so.conf.d/文件名.conf ,将自己写的动态库的路径放进去
- sudo ldconfig,更新配置文件
- 这种方式设置之后,重启依旧生效
- sudo rm /etc/ld.so.conf.d/文件名.conf
- sudo ldconfig,清理缓存
6.4.3 方法三: 软连接
- sudo ln -s 动态库的路径 /lib64/lib目标文件名.so