目录
-
- C文件接口
- 系统文件I/O
- 系统调用和库函数
- 文件描述符
-
- [0 & 1 & 2](#0 & 1 & 2)
- FILE和fd的关系
- 文件描述符的分配规则
- 重定向
- FILE
- 理解文件系统
-
- 了解磁盘的物理结构
- 逻辑抽象
- 文件系统
-
- 文件系统的图解和解析
- [通过文件系统来理解ls -al](#通过文件系统来理解ls -al)
- 通过文件系统来理解文件增删查改
- 补充细节
- 软硬链接
- 文件的三种时间
- 动态库和静态库
C文件接口
C文件接口都是封装了系统的文件接口,学习系统的文件接口有利于更熟悉文件的操作。
系统文件I/O
open函数
头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数描述
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
open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,则第三个参数表示创建文件的默认权限,否则只使用前两个参数。
打开文件的本质就是将需要的文件属性加载到内存中,OS内部一定会存在大量被打开的文件,那么操作系统如何管理被打开的文件?先描述,再组织。
先描述,构建在内存中的文件结构体struct file(文件属性,struct file*),每一个被打开的文件,都要在OS内构建对应的文件对象的struct结构体,再组织,即将所有的struct file结构体使用某种数据结构如链表去链接起来。
在OS内部,对被打开的文件进行管理,就被转换为了对链表的增删查改。
结论:文件被打开,OS要为被打开的文件,构建对应的数据结构。
struct file
{
//各自属性
//各种链接关系
};
文件可以分为两大类:磁盘文件,被打开的文件(内存文件)。
文件被打开,是谁在打开呢?OS,但是是谁让打开的?用户(进程为代表的),我们之前的所有文件操作,都是进程和被打开文件的关系,即struct task_struct 和 struct file的关系。
open函数的使用例子
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
int fd = open(LOG,O_WRONLY);//打开文件,表示只进行写入,如果没有该文件,会打开失败
if(fd == -1)
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
close(fd);
return 0;
}
没有log.txt文件,打开失败。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
int fd = open(LOG,O_WRONLY | O_CREAT);//打开文件,表示只进行写入,加入标准O_CREAT表示没有该文件就进行创建
if(fd == -1)
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
close(fd);
return 0;
}
虽然open函数创建了log.txt文件,但是ls显示的时候,该文件却标红,这是为什么呢?这是在创建的时候,没有设置权限,导致权限出现了乱码。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
int fd = open(LOG,O_WRONLY | O_CREAT,0666);//打开文件,表示只进行写入,加入标准O_CREAT表示没有该文件就进行创建
if(fd == -1) //加入权限0666
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
close(fd);
return 0;
}
此时,创建成功,权限也不会出现乱码,但是权限却是664,而不是666,这又是受到权限掩码的影响。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为0即可
int fd = open(LOG,O_WRONLY | O_CREAT,0666);//打开文件,表示只进行写入,加入标准O_CREAT表示没有该文件就进行创建
if(fd == -1) //加入权限0666
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
close(fd);
return 0;
}
这里的权限就是666了。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为0即可
int fd = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);//打开文件,表示只进行写入,加入标准O_CREAT表示没有该文件就进行创建
if(fd == -1) //加入权限0666,加入O_TRUNC表示打开文件时会对文件进行清空
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
close(fd);
return 0;
}
O_TRUNC表示每次打卡文件都会进行清空,它会O_APPEND即追加不会同时出现,因为两者相违。
open函数的flag标志
open函数的flag标志本质是一个位图结构,比如一个int就可以同时传递32个标记位,操作系统会去遍历这个位图,查看哪个位置是1,再进行相应的操作。比如是否清空,就表示位图某个位置是否置1。
系统调用和库函数
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc),而 open close read write lseek 都属于系统提供的接口,称之为系统调用口。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
这些函数直接man指令查询即可,与C库文件操作大同小异。
文件描述符
通过对open函数的学习,我们知道了文件描述符就是一个小整数
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。0,1,2对应的物理设备一般是:键盘,显示器,显示器。
任何一个进程,在启动的时候,默认会打开当前进程的三个文件。
C语言中:
extern FILE* stdin
extern FILE* stdout
extern FILE* stderr
我们打开一个文件,可以发现文件描述符是从3开始的。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
int fd = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd == -1)
{
printf("errno : %d , error : %s\n",errno,strerror(errno));
exit(-1);
}
printf("文件描述符:%d\n",fd);
close(fd);
return 0;
}
为什么文件描述符从3开始呢,0、1、2代表着什么,三个默认打开的文件流。
Linux下一切皆文件,所以向显示器打印,本质是向文件写入。
cpp
#include <cstdio>
#include <iostream>
int main()
{
printf("hello printf->stdout\n");
fprintf(stdout,"hello fprintf->stdout\n");
fprintf(stderr,"hello fprintf->stderr\n");
//c++
std::cout << "hello cout->cout" << std::endl;
std::cerr << "hello cerr->cerr" << std::endl;
return 0;
}
标准输入和标准错误都会向显示器打印,但是其实是不一样的。
标准输出会受到重定向的影响,标准错误不会受到重定向的影响。
进程如何管理打开的文件?
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
如何理解Linux下一切皆文件?
我们使用OS的本质:都是通过进程的方式进行OS的访问的。
在所有的外设上铺上一层软件层,即写函数和读函数,根据需要来设计这些函数,在上层,用struct file来管理这些函数,所以在Linux看来,一切皆文件。
FILE和fd的关系
FILE是什么?结构体。
谁提供的?C语言提供的。
和我们刚才提供的内核struct file有关系?------没有任何关系,上下层关系。
文件描述符的分配规则
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
int fd1 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd2 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd3 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd4 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd5 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd6 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
printf("%d\n",fd6);
return 0;
}
因为0、1、2是默认打开的三个文件流,所以是文件描述符是从3开始打开的。
c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
close(0);//关闭0号文件描述符
int fd1 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd2 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd3 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd4 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd5 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd6 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
printf("%d\n",fd6);
return 0;
}
关闭0号文件描述符,程序运行的结果:
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
close(0);//关闭0号文件描述符
close(2);//关闭2号文件描述符
int fd1 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd2 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd3 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd4 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd5 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd6 = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
printf("%d\n",fd6);
return 0;
}
关闭0号文件描述符和2号描述符,程序运行的结果:
进程中,文件描述符的分配规则:在文件描述符表中,最小的没有被使用的数组元素,分配给新文件。
重定向
重定向的本质
输出重定向
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
close(1);//关闭1号文件描述符
int fd = open(LOG,O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
printf("you can see me!\n");
return 0;
}
程序运行之后并没有结果,但是打印log.txt文件内容的时候,却发现了程序运行的结果。
这其实已经发生重定向。
程序首先关闭了1号文件,再打开log.txt,根据文件描述符的分配规则,log.txt将被分配到1号文件描述符。
printf往文件描述符为1(即标准输出)进行打印,但是底层文件描述符为1的指向从标准输出被替换为log.txt,所以出现了不往显示器打印,而是往log.txt文件输出。(重定向的本质)
重定向的原理:在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中的下标的指向。
输入重定向
将log.txt文件改为123 456
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);//这里将权限掩码设置为
close(0);//关闭0号文件描述符
int fd = open(LOG,O_RDONLY,0666);//注意改为只读
int a,b;
scanf("%d %d",&a,&b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
原理一样,关闭0号文件描述符,打开log.txt文件,那么0号文件描述符就指向log.txt文件,即从log.txt获取结果输入到变量a和变量b中。
图形跟上一张图差不多。只不过是标准输入指向的新文件。
追加重定向
让标准输出和标准错误分开输出到不同文件,方便排查错误。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG_NORWAL "logNormal.txt"
#define LOG_ERROR "logERROR.txt"
int main()
{
umask(0);//这里将权限掩码设置为
close(1);//关闭0号文件描述符
open(LOG_NORWAL,O_WRONLY | O_CREAT | O_APPEND,0666);//注意改为追加
close(2);
open(LOG_ERROR,O_WRONLY | O_CREAT | O_APPEND,0666);//注意改为追加
printf("printf->stdout\n");//正确结果
fprintf(stdout,"fprintf->stdout\n");//正确结果
fprintf(stderr,"fprintf->stderr\n");//错误
return 0;
}
重定向也可以通过指令来实现,常见的重定向有:>, >>, <。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
int main()
{
printf("printf->stdout\n");//正确结果
fprintf(stdout,"fprintf->stdout\n");//正确结果
fprintf(stderr,"fprintf->stderr\n");//错误
return 0;
}
stdout、cout都是向1号文件描述符对应的文件打印。
stderr、cerr都是向2号文件描述符对应的文件打印。
dup2函数
dup、dup2、dup3函数都是改变文件描述符的指向。
dup2(3,1);//将文件描述符1指向改为与文件描述符3指向相同,关闭文件描述符3的指向。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <iostream>
#define LOG "log.txt"
int main()
{
umask(0);
int fd = open(LOG,O_CREAT | O_WRONLY | O_APPEND,0666);
if(fd < 0)
{
perror("open");
exit(1);
}
dup2(fd,1);//将文件描述符1的指向改成与fd指向相同,关闭文件描述符fd的指向,间接完成了输出重定向
printf("hello world\n");
close(fd);
return 0;
}
FILE
我们先看一个程序。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define LOG "log.txt"
int main()
{
fprintf(stdout,"hello fprintf\n");
const char* msg = "hello write\n";
write(1,msg,strlen(msg));
fork();
return 0;
}
正常运行下:
输出重定向下:
输出重定向下,fprintf居然多打印了一次。这是为什么呢?
C语言会结合一定刷新策略,将我们在C库缓冲区中的数据写到files_struct[fd]指向的缓冲区,再由OS结合一定的刷新策略,将files_struct[fd]指向的缓冲区写到磁盘上。
C语言的刷新策略:
1.无缓冲
2.行缓冲(遇到\n就刷新)
3.全缓冲(缓冲区满了就刷新)
1.显示器采用的刷新策略:行缓冲。2.普通文件采用的刷新策略:全缓冲。
3.缓冲区在哪里,在你进行fopen打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中。
4.为什么要有缓冲区,节省调用者的时间(调用刷新函数是系统调用)(系统调用也要花费时间)(减少频繁调用,采用一次性刷新大量数据)。
解释现象
一开始只是运行程序,未重定向,结果为hello fprintf和hello write。
原因:fprintf是输入到显示器,显示器是行刷新,所以当把hello fprintf输入到C库中的缓冲区中,因为有'\n',所以会直接刷新到操作系统的缓冲区中,而write是系统调用,会直接刷新到操作系统的缓冲区中,综上,当fork的时候,C库的输出缓冲区已经没有内容了。
当输出重定向到普通文件时,普通文件是全刷新,而一行的hello fprintf\n并不会使C库的输出缓冲区满了,所以hello fprintf\n不会被刷新到操作系统的缓冲区中,而write是系统调用,会直接刷新到系统的缓冲区中,所以,fork的时候,C库的缓冲区还要内容hello fprintf\n,当fork的时候,C库的缓冲区还有内容hello fprintf\n,那么父子进程谁先进行刷新C库的缓冲区,谁先发生写实拷贝,那么父子进程各刷新一次,就会输出两次hello fprintf\n。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的"上层", 是对系统调用的"封装",但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
前面已经说过,由OS结合一定的刷新策略,将files_struct[fd]指向的缓冲区写到磁盘上,那么,我们怎么强制刷新内核缓冲区呢?使用fsync函数,就可以强制内核缓冲区刷新数据到磁盘上。
int fsync(int fd);
理解文件系统
如果文件没有被打开呢?那么文件会在哪里?一定不在内存中,只能在磁盘等外设中静静的存储着。
磁盘文件,如何没有被打开,如何理解这些文件呢?需要解决什么问题?
需要解决合理存储的问题,快速定位,快速读取和写入等问题。
了解磁盘的物理结构
磁盘分为机械磁盘和SSD固态磁盘。
磁盘是计算机唯一的一个机械设备,同时它还是外设。外设和机械设备决定了它非常慢(相对CPU和内存来说)。
盘片:一片两面,有一摞盘片,可以读写。
向磁盘写入:盘片的某一些位置N->S
删除磁盘数据:磁盘的某一些位置S->N
磁头:一个面一个磁头,一个磁头负责一面的读取
磁头:共进退的。
磁头和盘面是没有挨着的,距离依旧很近。
对数据的写入和读取分别是更改某个位置的S、N或者读取某个位置的S、N。
磁盘中存储的基本元素:扇区,512字节或者4kb字节,一般磁盘,所有的扇区都是512字节。
同半径的所有扇区,称为磁道。
如何定位一个扇区?
首先确定哪一面,这里只需要确定哪一个磁头的编号来确定是哪一面。
再定位哪一个磁道,由半径决定。
再确定哪一个扇区,根据扇区的编号,确定一个扇区。
磁头:head
柱面(磁道):cylinder
扇区:sector
所以上面确定扇区的办法为:CHS定位法
一个普通文件(属性和数据),都是数据(0,1),无非就是占用一个或者多个扇区,来进行自己的数据存储的。既然能用CHS方法定位任意一个扇区,就可以定位任意多个扇区,从而将文件从硬件角度,进行读取或者写入。
逻辑抽象
如果OS能够得知任意一个CHS地址,就能访问任意一个扇区。那么OS内部是直接使用CHS地址的么?不是。
1.OS是软件,磁盘是硬件,磁盘定位一个地址,CHS,OS使用的话,如果硬件变了,OS也要发生变化,OS和磁盘没有做好解耦的工作。
2。即使是扇区,512字节,定位的IO的基本数据也就很小,OS基本单位是4kb(可以调整),磁盘:块设备,所以,OS要有一套新的地址,来进行块设备级别的访问。
OS是以4kb为单位进行IO的,故一个OS级别的文件块要包括8个扇区,甚至,在OS角度,它不关心扇区。
计算机常规的访问方式:起始地址+偏移量(语言中的数据类型),只需要找到数据块的起始地址(第一个扇区的下标地址)+4kb(块的类型),就可以定位一个数据块。
OS采用LBA(逻辑块地址),磁盘采用CHS地址,所以一直存在LBA和CHS相互转换。
文件系统
文件系统的图解和解析
Boot Block:保存与操作系统启动相关的内容,诸如分区表和操作系统的镜像的地址,一般该分区会在0号盘面的0号磁道的1号扇区里开始保存,对应的一般在C盘的某个位置存在这样的数据,当操作系统开机的时候,至少要做两件事情,第一个,找到磁盘这个设备,并加载磁盘的驱动程序,第二个是加载分区表,识别出磁盘的C盘、D盘、F盘,再从分区的起始位置读取操作系统的地址,然后找到操作系统在磁盘的位置,再加载操作系统。如果这个块因为某一些原因导致数据丢失,比如刮花了,那么操作系统就会挂掉。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。总结:1.文件系统的类型 2.整个分组的情况。
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表(inode Table):存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容
一般而言,一个文件内部所有属性的集合为inode节点(128字节),一个文件,一个inode,其中,即使是一个分区,内部也会存在大量的文件即会存在大量的inode节点,一个group,需要有一个区域,来专门保存该group内所有的inode节点------inode table。
分组内部,可能会存在多个inode,需要将inode区分开来,每一个inode都会有自己的inode编号,inode编号,也属于对应文件的属性id。
文件的内容是变化的。文件系统采用数据块来进行文件内容的保存,所以一个有效的文件,要保存内容,就需要[1,n]个数据块。
linux查找一个文件,是要根据inode编号,来进行文件查找的,包括读取内容。
一个inode对应一个文件,而该文件inode属性和该文件的数据块是有映射关系的。
文件=内容+属性
linux是将内容和属性分离的,都是以块的形式,被保存在磁盘的某个位置的。
数组代表了该文件对应的数据块的编号,比如Data blocks依次保存了该文件的位置是1、3、5、7、9,那么该数组存的是1、3、5、7、9。
操作系统加载一个文件的时候,一次会加载许多的inode Table,因为它大小固定,并且比较小,加载进来以后,访问哪个文件不着急,因为inode Table有跟文件的映射关系,需要读取文件的哪一块,再根据inode属性里面的数组,在块里面去找,拿到lba转换成chs去访问磁盘。
ls -il //查看文件的inode
通过文件系统来理解ls -al
ls -al查文件,那么文件的查是怎么操作的呢?当ls进程跑起来,它所处的路径和目录已知,那么通过该目录(目录也是文件)的inode与数据块的对应关系,找到该目录存储数据的数据块,找到对应的文件名和inode的映射关系,找到inode,然后在自己特定的分区中找到对应的inode数据。
目录的数据块存储着该目录下的文件和inode的对应关系。
- inode vs 文件名
linux系统只认识inode编号,文件的inode属性中,并不存在文件名,文件名,只是给用户用的。 - 重新认识目录
目录是文件吗?是的。目录有inode吗?有。
有内容吗?有。
内容是什么? - 任何一个文件,一定在一个目录的内部,所以目录的内容是什么呢?存储需要数据块,目录的数据块里面保存的是该目录下的文件名和文件inode编号的对应的映射关系,而且,在目录内,文件名和inode互为key值。
- 当我们访问一个文件的时候,我们是在特定的目录下访问的 cat log.txt
a.先要在当前目录下,找到log.txt的inode编号。
b.一个目录也是一个文件,也一定隶属于一个分区,结合inode,在该分区中找到分组,在该分组中inode table中,找到文件的inode。
c.通过inode和对应的data blocks的映射关系,找到该文件的数据块,并加载到OS,并完全显示到显示器。
通过文件系统来理解文件增删查改
删除一个文件
1.根据文件名在该文件所在目录的数据块找到对应的inode编号
2.经过inode与data blocks的映射关系,设置block bitmap对应的比特位,置0就可以
3.inode,设置对应的inode bitmap对应的比特位为0
总结:删除文件只需要修改位图即可。
增加一个文件
当增加文件的时候,首先在一个特定的目录下创建,操作系统就要在该目录所在某一个分组中,先去查inode bitmap,从低比特位向高比特位扫描,找到为0的比特位,把它置为1,置1的同时根据是第几个比特位,得到新的inode编号,然后把文件相关的属性填到inode Table中的inode表中,文件刚开始是空的,所以没有数据块,找到目录的数据块,追加一条新的文件名和inode的映射关系,然后这个文件就创建好了,未来你想对文件做修改和写入的时候,拿到文件名找映射关系,然后找到该文件的inode,分配Data blocks,操作系统就会即使想要刷新的数据有多大,再申请若干个数据块,并把block bitmap对应的位置置为1,最后进行刷新数据。
查找一个文件
如find指令,操作系统会在特定的目录下,通过该目录的inode与data blocks的映射关系,找到对应的数据块,可查看该目录下的inode和文件名的映射关系,找到想要查找的文件,便可以得知该文件的属性(inode)和内容(对应的data blocks)。
修改一个文件
在特定的目录下,通过该目录的inode与data blocks的映射关系,找到该目录下的数据块,可查到该目录下inode和文件名的映射关系,查到想要修改的文件的inode,通过inode与data blocks的对应关系,找到该文件的数据块,便可以进行修改,修改后,操作系统自动修正inode的属性。
补充细节
-
如果文件被误删了,该怎么办?
文件被误删了,那么就不要创建新的文件和目录,因为被误删文件的位图由1变为0,意味着该文件对应的inode节点和数据块随时都会被新的文件占用,所以文件误删,最好的做法就是什么都不要干(即不要把数据和属性覆盖),如果能力允许的话,利用一些工具,找到被删文件的inode编号,通过inode编号找到特定的分组,然后将inode bitmap对应的比特位由0置为1,然后读取inode表,提取读取文件占有的数据块,然后将对应的数据块的block bitmap置1。
-
inode,确定分组,inode是在一个分区唯一有效的,不能跨分区。(每个分区都有自己的文件系统)
-
分区和分组,填写系统属性是谁做的呢?OS做的。什么时候做的呢?分区完成之后,要让分区能够被正常使用,我们需要对分区做格式化处理。格式化的过程,其实是OS向分区写入文件系统的管理属性信息。
-
如果inode只是单单的用数组建立和data block的映射关系,15 * 4kb = 60kb(假设一个datablock数组有15个元素),是不是意味着一个文件最多放入60kb。
格式化时,如果整个分区从来没有格式化过,那么格式化,需要去写数据区(从SB到Data blocks),如果当前有数据区,格式化的时候只需要将位图结构清空,属性字段(sb、gdt)设置为初始状态就可以了。inode table和data blocks不管。
Linux对于删除文件会有一个日志,该日志会记录文件名和inode,这个日志会保留一段时间。
每个分区的都有一套inode机制,inode不能跨分区使用,inode怎么确定分组呢?比如inode有100万个,那么可能把10万个inode分配给第一个分组,那么只要0到10万减一的inode就在第一个分组
- 有没有可能,一个组的数据块没用完,inode没了,或者inode没用完,data blocks用完了?
有可能,比如在分区中创建大量的空文件,那不就是在消耗inode么。建造一个文件,疯狂的往该文件塞大量的数据,不就是在消耗data blocks么。
操作系统启动时,预加载除了data blocks的其它内容。
软硬链接
1.制造软硬链接,对比差别
软链接文件的名字应该是my_soft。
软链接是一个独立的链接文件,有自己的inode number,必有自己的inode属性和内容,其中内容放的是自己所指向文件的路径,类似windows的快捷方式。
硬链接是和目标文件共用一个inode number,意味着硬链接一定和目标文件使用同一个inode的,硬链接没有独立的inode。那硬链接干了什么?建立了新的文件名和老的inode的映射关系。
硬链接,采用引用计数,当引用计数为0时,才真正删除文件,不然都是引用计数减一。
unlink my_hard 删除硬链接,unlink可用来删除普通文件。
2.软硬链接的使用场景
在ls查询中,权限后面跟的数字就是引用计数,硬链接某个文件以后,该文件的这个数字会增加。
3.补充知识
在创建一个新的目录的时候引用计数是2,是因为原来文件名与inode的映射关系和.文件名和inode的映射关系。
ls -di //-d 将文件像目录一样显示 -i 输出文件索引,也就是inode
不能给目录建立硬链接,为什么?
因为容易造成环路路径问题。
假如我从根目录开始,进行深度优先遍历查找,当查找到/home/zrb/107/test的时候,再往link_hard目录查找的时候,又因为link_hard是test的硬链接,所以本质是又回到了test目录,又再往link_hard的目录查找,导致了深度优先遍历一直卡在了这条查找路径上。
上级目录...本质也是一种环路路径问题,/home/zrb/107/test/. . 注意. .是在下一个目录里面的,表示当前目录的,所以还是跟上面的情况是一样的,深度优先遍历的时候从test目录进去. .目录,但由于. .目录是test目录的硬链接,所以本质又是回到了test目录,继续往. .目录遍历,导致一直卡在了这条搜索路径,但是,操作系统会对遇到. .进行特殊处理,所以才不会死循环查找。
文件的三种时间
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间
增删查改中,查的频率是最高的,如果高频次的对文件最后访问时间做修改的话,一定意味着高频次的将修改后的时间刷新到磁盘外设中,将提高系统刷新的IO频次,进而导致效率降低,所以在最新的内核中把查看的时间修改策略改变了,也就是不一定每次查看都要修改文件最后访问时间,可能每隔上三五次刷新一层,不同的操作系统的刷新策略不同,进而减少内存和磁盘IO交涉的成本,修改时间每次都要刷新,访问数据不修改文件内容和属性,即使丢失了,也没有关系,但是修改时间丢失了,就有问题了。
动态库和静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
头文件和库的关系
1.系统已经预装了c/c++的头文件和库文件,头文件提高方法说明,库提高方法的实现,头和库是有对应关系的,是要组合在一起使用的。
2.头文件是在预处理阶段引入的,链接的本质是链接库。
理解现象:
a.在vs2019、vs2022下安装开发环境------安装编辑器软件、安装要开发的语言配套的库和头文件。
b.我们在使用编辑器,都会有语法的自动提醒功能,需要先包含头文件的。语法提醒的本质:编辑器会主动的将用户输入的内容,不断的在被包含的头文件中进行搜索,自动提醒功能是依赖头文件的。
c.我们在写代码的时候,我们的环境怎么知道我们的代码有哪些地方有语法错误,哪些地方定义变量有问题?编辑器有命令行模式和其他自动化模式帮我们不断的进行语法检查。
为什么要有库
提高开发效率------可以选择源代码、或者编译成二进制代码给别人。
写一个库
当我们写了一个库,要将库引入我们的项目,必须让编辑器找到头文件+库文件。
在下面的解释中,全部按照自己实现了代码,如何给别人使用的背景下来进行操作。
目录myself就是代表自己,目录otherperson就是代表其他人
我在目录myself实现了几个函数,如下
myadd.h文件
cpp
#ifndef __ADD_H__
#define __ADD_H__
int add(int a,int b);
#endif
myadd.c文件
cpp
#include "myadd.h"
int add(int a,int b)
{
return a + b;
}
mysub.h
cpp
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a,int b);
#endif
mysub.c
cpp
#include "mysub.h"
int sub(int a,int b)
{
return a - b;
}
接下来就是如何将myself目录下的文件给otherperson使用。
直接拷贝头文件和源代码给别人
此时,其他人便可以调用函数的方式去实现。
cpp
#include <stdio.h>
#include "myadd.h"
#include "mysub.h"
int main()
{
printf("%d\n",add(10,15));
printf("%d\n",sub(20,15));
}
但是,一般来说,并不会直接把源代码拷贝给其他人,保障知识产权。
将源代码生成二进制文件,拷贝给别人
其他人也可以通过这种方式使用你写的函数
生成静态库,拷贝给别人
其他人也可以通过这种方式使用你写的函数
生成静态库,压缩发给别人
其他人通过解压,便可以进行调用我写的函数了。
将头文件拷贝到系统搜索头文件的默认搜索位置和将库拷贝到库的默认搜索路径下
其他直接表明使用的库名字,便可以进行调用我写的函数了。
生成动态库,拷贝给别人,别人将库路径导入环境变量当中,让操作系统可以查找,就可以运行
别人便可以通过动态库,调用我写的函数了
生成动态库,拷贝给别人,别人建立动态库的软链接,就可以运行
防止上面环境变量影响,可以重启xshell。
别人便可以通过动态库,调用我写的函数了
生成动态库,拷贝给别人,别人将动态库的路径拷贝到配置文件里,就可以运行
防止上面的软链接影响,可以取消软链接。
/etc/ld.so.conf:记录了程序加载运行期间查找动态链接库时的路径。
ld:链接
so:动态库
conf:配置文件
配置文件里面放动态库的路径。
别人便可以通过动态库,调用我写的函数了
第三方库的使用
- 需要指定头文件和库文件
- 如果没有默认安装到gcc、g++的默认搜索路径下,用户必须指明对应的选项,告知编译器:
a.头文件在哪里
b.库文件在哪里
c.库文件是谁 - 将我们下载下来的库和头文件,拷贝到系统的默认搜索路径下------就是在Linux下安装库。那么卸载呢?对任何软件,安装和卸载的本质就是拷贝到系统特定的路径下。
- 如果我们安装的库是第三方的(语言,操作系统系统接口)库,我们要正常使用,即使是已经全部安装到了系统中,gcc/g++必须要用-l指明具体库的名称。
理解现象:在Linux安装的大部分指令,都是需要sudo或者超级用户操作的。
动态库配置
运行时,OS是如何查找动态库的
- 环境变量:LD_LIBRARY_PATH,每次打开xshell,环境变量都会被重新继承,加入的环境变量会生效,所以是临时方案。
- 软链接方案
- 配置文件方案
在生成静态库,拷贝给别人的实验中,我们直接告诉编辑器库在哪里,就可以运行了,但是动态库这里,需要环境变量或者软链接或者配置文件呢?
gcc -o myfile test.c -I include -L lib -lmymath
上面那条指令只是告诉了编译器,没有告诉OS,而上面三种方案,就是告诉OS,库在哪里,但是,为什么静态库直接就可以运行了呢?是因为,静态库,链接原则:将用户使用的二进制代码直接拷贝到目标可执行程序中,但是动态库不会。
静态库的理解
静态链接形成的可执行程序,本身就有静态库中方法的实现(非常占用资源,可执行程序体积变大,加载占用内存,下载周期变长,占用网络资源)
动态库的理解
将可执行程序中的外部符号,替换成为库中的具体的地址。
假设程序用到了c库的printf方法,并且当前只有动态库
代码1、代码2、代码3属于mymath的代码。
当进程开始沿着代码1、代码2往下执行时,遇到printf,经过页表映射,发现没有printf代码,所以整个进程结合操作系统当中立马意识到这个程序依赖动态库,所以在当前操作系统检索动态库,将动态库中的printf方法load到内存中,并且建立映射关系,映射到共享区中。
执行代码1、代码2,正常通过页表映射,找到物理内存中的mymath,执行对应的代码,当执行到printf时,操作系统就会检测到printf已经加载进来,并映射成功,那么将从共享区找到printf的虚拟地址,通过页表映射,找到物理内存存储的printf方法,运行该printf方法,然后再回来运行原有的代码。
在加载printf方法时,操作系统会以4kb为单位去加载,即操作系统会将printf函数上下文代码也加载进来,然后就可以加速其他进程或者它自己代码当中还会用到printf周边的方法,所以才会这样加载。
只要把库中对应的方法加载到内存,映射到进程的地址空间之后,我们的代码执行库中的方法,就依旧还是在自己的地址空间内进行函数跳转即可。
当加载进程B,进程B依然使用了printf方法,当进程B使用printf,操作系统检测到已经加载了printf方法到物理内存,此时,直接将内存中的printf方法映射到地址空间即可。其他进程如果使用printf方法时,也只是直接将内存中的printf方法映射到地址空间即可,共享库的所有公共方法在内存里,最终只要一份就可以了,剩下的要用哪一些,就映射哪一些。
库可能很大,平时进程不一定会使用库里面所有的方法,所以说把库加载到内存,不代表同时可以把库全部加载到内存里,需要哪一个,就加载哪一个,取决于操作系统的策略。
静态库和动态库加载进地址空间的地址规律
在动态库加载中,不同的进程,运行程度是不同的,需要使用的第三方库是不同的,注定了,每一个进程的共享地址空间中的空闲位置是不确定的。如果采用固定地址映射,比如printf方法在进程A映射的地址是0x11223344,但是在进程B,0x11223344的地址空间已经被用了,那么就没有法映射0x11223344了。
在程序编译链接形成可执行程序的时候,可执行程序内部有没有地址?有。进程地址空间就已经提到了。
绝对编址:例如,我在操场的50米左右。
相对编址:我在操场的树的左边大约20米。
当一个库真正被映射进地址空间的时候,它的起始地址才能真正确定。
调用printf的时候,只需要知道调用哪一个库
如:这里调用lib.so库
该库的真正起始地址+123就可以找到printf方法
该库的真正起始地址是什么?就是映射到进程地址空间的起始地址。操作系统会记录下来。
动态库,在进程的地址空间中,随便加载,与我们加载到进程地址空间的什么位置,毫无关系了。
与位置无关码:动态库中的地址都是偏移量。
好处:如库有1万个函数,而这份代码只用到其中100个方法,可以实现局部性加载,需要哪些,加载哪些。
动态库和静态库都存在,默认动态链接
在前面的操作中,已经存在一些测试文件了。
上面可以验证,动态库和静态库都存在,默认动态链接。
那么如何强制静态链接呢?
如果不提供动态库,只提供静态库,那么程序在进行动态链接的时候(没有加-static),没办法,只能将库以静态链接的方式进行链接,但是程序不仅仅链接一个库,还要链接C库,所以其他库依旧动态链接,所以在一个程序中,链接并非只有一种。