回顾C语言文件接口
C语言文件操作函数
函数 | 功能 |
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
写入操作
#include<stdio.h>
int main()
{
FILE *fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
int count = 5;
while(count)
{
fputs("hello world\n",fp);
count--;
}
fclose(fp);
return 0;
}
读取操作
#include<stdio.h>
int main()
{
FILE *fp = fopen("log.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[64];
int i;
for(i = 0;i<5;++i)
{
fgets(buffer,sizeof(buffer),fp);
printf("%s",buffer);
}
fclose(fp);
return 0;
}
当前路径的定义
当fopen以写入的方式打开一个文件时,若该文件不存在,就会自动在当前路径创建该文件,那么这里所说的当前路径是什么呢?
在上面例子中,我们在BaseIO目录下运行可执行程序in,那么该可执行程序创建的log.txt文件会出现在baseIO目录下,此时可以确定当前目录就是程序in所在的路径吗?
我们在上级目录中运行in文件:
可以看到,log.txt并没有在baseIO目录下创建文件,而是在当前的目录创建文件。
当程序运行成进程的时候,我们可以获取进程的PID,然后根据该PID在根目录下的proc目录下查看进程信息:
会发现有两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,exe就是程序所处的路径。
综上所述:当前路径不是指可执行程序所处的路径,而是可执行程序运行时我们所处的路径。
默认打开的三个流
在Linux中任何东西都可以看成文件,所以显示器和键盘也可以看成文件,我们能在显示器上看到数据,是因为我们向“显示器”这个文件写入了数据,电脑能获取到点击键盘对应的字符,是因为电脑从“键盘”这个文件读取了数据。
问题来了:为什么我们不需要打开这些文件呢?
实际上,任何进程运行时都会默认打开三个输入输出流:标准输入流,标准输出流,标准错误流,对应到C语言中就是stdin,stdout,stderr。
其中,标准输入流对应的就是键盘,标准输出流和标准错误流对应的就是显示器。
我们可以通过查看man手册就可以发现,stdin、stdout、stderr这三个其实都是FILE*类型的。
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
当C语言程序被运行起来时,操作系统会默认使用C语言相关接口将这三个输入输出流打开,然后我们才能使用scanf和printf之类的函数与键盘和监视器进行互动操作。
所以,这三个输入输出流其实是和我们打开文件时获取到的文件指针时一个概念,我们可以通过下列代码试验:用fputs将第二个参数设置为stdout,看看是否会在显示器上显示。
#include<stdio.h>
int main()
{
fputs("this is stdin\n",stdout);
fputs("this is stdout\n",stdout);
fputs("this is stderr\n",stdout);
return 0;
}
运行结果:
可以看到,确实是显示在了显示器上。
需要注意的是:不只是C语言有输入输出流,C++中也有对应的cin cout cerr。其它语言中也有类似的概念,这是由操作系统所支持的。
系统文件I/O
操作文件除了C语言接口和c++接口或者别的接口外,还有一套自己的系统接口来进行文件的访问。
相比于C库函数或者其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。
我们在Linux系统下运行C语言代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,可以方便二次开发。
open
系统接口中使用open函数打开文件,open函数的函数原型如下:
int open(const char *pathname, int flags, mode_t mode);
open的三个参数
第一个参数:pathname,表示要打开或创建的目标文件
1.若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
2.若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
第二个参数:flags,表示打开文件的方式
常用的flags有下列几个:
参数选项 | 含义 |
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
当想传入多个参数选项时,可以将这些选项用“ | ”运算符隔开。
例如,想以只写的方式打开文件,要是不存在的话就自动创建文件,则设置如下:
O_WRONLY | O_CREAT
第三个参数:mode,表示创建文件的默认权限
例如,将mode设置为0666,则文件创建出来的权限如下:
- r w - r w - r w -
其实创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建的文件权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际的文件权限为0664
- r w - r w - r - -
如果想创建出来文件的权限值不受umask的影响,那么需要在创建文件前使用umask函数将文件默认掩码设置为0
umask(0)//将文件默认掩码设置为0
如果不需要创建文件,那么open的第三个参数可以不设置。
open的返回值
open函数的返回值时先打开文件的文件描述符。
我们可以在linux中尝试一次打开多个文件,然后分别打印它们的文件描述符
代码如下:
运行结果如下所示:
可以看到,文件描述符从3一直打印到5连续递增,这是为什么呢?实际上这里的文件描述符是一个指针数组的下标,指针数组中的每个指针都指向一个被打开文件的文件信息,通过文件描述符就可以找到对应的文件信息。
当我们用open函数打开文件成功之后,,数组中的指针个数增加,然后返回该指针在数组中的下标,如果打开失败的时候会返回-1。所以,成功打开多个文件的文件描述符时递增的。
那为什么时从3开始呢?
前面说到,在Linux中有三个输入输出流,分别是stdin、stdout和stderr,他们分别占用了数组下标为 0 1 2 的位置。这就是为什么我们前面的文件描述符是从3开始的了。
close
系统接口中使用close函数关闭文件,close函数的函数原型为:
int close(int fd);
使用close函数时只需要传入需要关闭文件的文件描述符即可,如果成功关闭则返回0,若关闭文件失败则返回-1。
write
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
函数理解:从buf位置开始向后count字节的数据写入文件描述符为fd的文件中。
如果数据写入成功,返回实际写入数据的字节个数。
如果写入失败,返回-1
我们可以运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* msg = "hello syscall\n";
int i;
for(i = 0;i<5;++i)
{
write(fd, msg ,strlen(msg));
}
close(fd);
return 0;
}
运行程序之后,我们可以用cat函数获取到我们写入的内容。
read
系统接口中使用read函数从文件读取信息,read函数原型为:
ssize_t read(int fd, void *buf, size_t count);
函数解释:从文件描述符为fd的文件读取count字节的数据到buf位置当中。
如果读取成功,返回实际读取数据的字节个数。
如果读取失败,返回-1。
我们可以运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char ch;
//循环打印
while(1)
{
ssize_t s = read(fd, &ch, 1);
//当读取失败时,说明已经读取完毕,可以中断了
if(s <= 0)
{
break;
}
//读取到的字符,用write函数向显示器文件输入
write(1, &ch, 1);
}
close(fd);
return 0;
}
运行结果如下:
文件描述符fd
文件时由进程运行时打开的,由于一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
因此,操作系统必须要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,这些结构体会以双链表的形式链接起来,之后操作系统对这些文件的管理就变成了对双链表的管理。
为了区分已经打开的文件哪些时属于特定的某一个进场,我们还需要建立进程和文件之间的对应关系。
那么该如何建立这个关系呢?
我们在前面学到过,当一个程序运行起来时,系统会为该程序的代码和数据加载到内存,然后创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
在task_struct中,有一个指针,指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
如上面对于文件操作的过程:当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,然后将该结构体的首地址填入到fd_array数组下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该下标给进程。
文件描述符的分配规则
我们从上面的例子拿下来看,先运行下列代码:
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt",O_RDONLY | O_CREAT,0666);
int fd2 = open("log2.txt",O_RDONLY | O_CREAT,0666);
int fd3 = open("log3.txt",O_RDONLY | O_CREAT,0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
return 0;
}
运行结果如下:
我们现在知道, 0 1 2 都被输入输出流给占用了,只能从3开始分配。
如果我们在打开这三个文件前,先关闭文件描述符为0的文件,那么分配会是怎么样的呢?
close(0);
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
umask(0);
//关闭fd为0的文件
close(0);
int fd1 = open("log1.txt",O_RDONLY | O_CREAT,0666);
int fd2 = open("log2.txt",O_RDONLY | O_CREAT,0666);
int fd3 = open("log3.txt",O_RDONLY | O_CREAT,0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
return 0;
}
运行结果如下:
可以看到,此时fd1变成0了,之后的还是从3开始。
我们再试试将 0 和 2关闭(不能关闭1,1是输出流,也就是显示器文件,关闭了就看不到任何信息了)
close(0);
close(2);
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main()
{
umask(0);
//关闭fd为0和2的文件
close(0);
close(2);
int fd1 = open("log1.txt",O_RDONLY | O_CREAT,0666);
int fd2 = open("log2.txt",O_RDONLY | O_CREAT,0666);
int fd3 = open("log3.txt",O_RDONLY | O_CREAT,0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
return 0;
}
运行结果如下:
可以看到,现在fd1为0,fd2为2,其余的从3开始。
综上所述:文件描述符时从没有使用过的最小的fd_array数组下标开始分配的。
重定向
重定向的原理
在了解了文件描述符的概念和分配之后,我们可以学习更深入的——重定向原理
输出重定向
原理:将本应该输出到一个文件的数据重定向输出到另一个文件中。
可以举个例子:我们将本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,此时我们打开log.txt文件分配到的第一个描述符就是1——显示器文件。
运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//关闭文件描述符为1的文件
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果如下:可以看到,并没有输出到显示器,只有用cat函数才能看到原本应该输出到显示器的输出到了log.txt文件里。
这里有两点要说明:
1.printf函数时默认向stdout输出数据的,而stdout时指向struct FILE类型的结构体,这个结构体有一个存储文件描述符的变量,也就是 1 ,所以printf实际上时向文件描述符为1的文件输出数据。
2.为什么会有fflush函数,没有的话log.txt是不会输出三行hello world的,因为C语言的数据并不是马上写到内存里,而是写到C语言的缓冲区,所以使用printf之后需要用fflush函数将缓冲期里的数据刷新到文件中。
追加重定向
原理:于输出重定向类似,输出是覆盖式输出数据,而追加是追加式输出数据。
此时,我们打开文件就需要用到:O_APPEND
int fd = open("log.txt",O_WRONLY | O_APPEND | O_CREAT, 0666);
运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//关闭文件描述符为1的文件
close(1);
int fd = open("log.txt",O_WRONLY | O_APPEND | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果如下:
输入重定向
原理:将本应该从一个文件中读取数据重定向为另一个文件读取数据。
举个例子,我们本应该从“键盘文件”中读取数据的scanf函数,可以改为从log.txt中读取数据,我们只要在打开文件之前将文件描述符为0的文件关闭,这样就可以了。
运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
close(0);
int fd = open("log.txt",O_RDONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
char str[50];
while(scanf("%s", str) != EOF)
{
printf("%s\n", str);
}
close(fd);
return 0;
}
运行结果如下:可以看到,这样就能把log.txt文件的数据全读出来了
特殊说明:这里的scanf原理和printf的原理类似,只是scanf的stdin的文件描述符时0,所以scanf是向文件描述符为0的文件读取数据。
dup2
通过上面的学习我们可以知道,我们要对什么重定向,我们就让需要重定向的文件的数据占据要被重定向的文件所在的数组下标中:也就是我们将fd_array[3]对应的文件数据拷贝到fd_array[1]中,那么我们此时就是输出重定向到文件描述符为3的文件。
而在Linux中,操作系统提供了系统接口dup2,我们可以使用dup2函数来完成重定向
int dup2(int oldfd, newfd);
功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]中,如果有必要的话需要先关闭文件描述符为newfd的文件。
返回值:dup2调用成功:返回newfd,调用失败:返回-1.
duo2有两个需要注意的点:
1.如果oldfd是无效的文件描述符,那么会调用失败,此时newfd也不会被关闭。
2.如果oldfd有效,但是newfd和oldfd相同,那么不会进行任何操作,正常返回newfd。
=举一个例子:我们在将log.txt的文件描述符fd和1传入dup2函数,那么dup2将会把fd_array[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,那么此时要输出到显示器的数据就会输出到log.txt文件里。
可以运行下列代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("this is printf\n");
fprintf(stdout,"this is fprintf\n");
return 0;
}
运行结果如下:
添加重定向功能到myshell
#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
int type = 0; //0 >, 1 >>, 2 <
char cmd[LEN]; //存储命令
char* myargv[NUM]; //存储命令拆分后的结果
char hostname[32]; //主机名
char pwd[128]; //当前目录
while (1)
{
//获取命令信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname,sizeof(hostname)-1);
getcwd(pwd,sizeof(pwd)-1);
int len = strlen(pwd);
char *p = pwd + len -1;
while(*p != '/')
{
--p;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$",pass->pw_name, hostname, p);
//读取命令
fgets(cmd,LEN,stdin);
cmd[strlen(cmd)-1] = '\0';
//实现重定向功能
char* start = cmd;
while (*start != '\0'){
if (*start == '>'){
type = 0; //一个>说明为输出重定向
*start = '\0';
start++;
if (*start == '>'){//遇到第二个>,为>>
type = 1; //两个>,为>>,为追加重定向
start++;
}
break;
}
if (*start == '<'){
type = 2;
*start = '\0';
start++;
break;
}
start++;
}
if (*start != '\0'){ //start位置不为'\0',说明命令包含重定向内容
while (isspace(*start)) //跳过重定向符号后面的空格
start++;
}
else{
start = NULL; //start设置为NULL,标识命令当中不含重定向内容
}
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //创建子进程执行命令
if (id == 0)
{
//child
if (start != NULL)
{
if (type == 0)
{
//输出重定向
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); //以写的方式打开文件
if (fd < 0)
{
error("open");
exit(2);
}
close(1);
dup2(fd, 1);
}
else if (type == 1)
{
//追加重定向
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); //以追加的方式打开文件
if (fd < 0)
{
perror("open");
exit(2);
}
close(1);
dup2(fd, 1);
}
else{
//输入重定向
int fd = open(start, O_RDONLY); //以读的方式打开文件
if (fd < 0)
{
perror("open");
exit(2);
}
close(0);
dup2(fd, 0);
}
}
execvp(myargv[0], myargv); //child进行程序替换
exit(1); //替换失败的退出码设置为1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0)
{
printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
}
}
return 0;
}
FILE
FILE当中的文件描述符
前面学习了,库函数实际上就是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库中的FILE结构体内部肯定是有这个文件描述符fd的。
首先,我们可以在 /usr/include/stdio.h 头文件中看到下列代码:
typedef struct _IO_FILE FILE;
而我们在 /usr/include/libio.h 头文件中可以找到 struct _IO_FILE 结构体的定义,在这里可以看到一个叫做 _fileno 的成员,这个成员就是封装的文件描述符。
那么C语言的fopen函数在做什么呢?
fopen函数为用户申请FILE结构体变量,然后返回结构体的地址(FILE*),在底层通过接口open打开对应文件,得到文件描述符fd,并把fd填充到FILE结构体中的 _fileno 变量中,最后完成文件打开操作。
当然,C语言的其他文件操作函数,例如fread、fwrite、fputs、fgets等,都是先根据传入的文件指针找到对应的FILE结构体,然后再找到文件描述符,最后通过fd对文件进行操作的。
FILE当中的缓冲区
我们先来看下列代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
//C库函数
printf("this is printf\n");
fputs("this is fputs\n", stdout);
//系统接口
write(1, "this is write\n", 14);
fork();
return 0;
}
运行结果如下:
如果我们将结果重定向到 log.txt 中:
可以看到,这两个的结果是不同的。在C库函数打印的内容重定向到文件后就变成两份,而系统接口打印的内容还是原来的一份。
我们要知道,缓冲方式有三种:
1.无缓冲
2.行缓冲(对显示器进行刷新数据)
3.全缓冲(对磁盘文件进行写入数据)
当直接运行可执行程序时,将数据打印到显示器时就是行缓冲,因为代码后面都有\n,所以会立刻将数据刷新到显示器上。
当我们重定向到 log.txt 文件时,数据的缓冲方式就变成了全缓冲,此时printf和fput的数据都打印到了缓冲区中,当我们用for函数创建子进程时,结合之前所学的知识,进程间有独立性,当父子进程要刷新缓冲区内容时,就是在修改数据,那么此时要对数据进行写实拷贝,那么此时缓冲区数据就变成两份,那么printf和fputs函数就有两份了,因为write是系统接口,可以看成没有缓冲区,所以只有一份数据。
缓冲区是谁提供的?
这个缓冲区时C语言自带的,如果是系统提供的话,系统接口的write函数也应该打印两次。
缓冲区在哪?
printf函数将数据打印到stdout里,stdout就是一个FILE*指针,在FILE结构体当中还有一大部分成员时用来记录缓冲区相关信息的。
也就是说,缓冲区是C语言提供,然后在FILE结构体中进行维护的,FILE结构体当中不仅保存了队友文件的文件描述符,也保存了用户缓冲区的相关信息。
那么,操作系统有缓冲区吗?
有的,我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或者显示器上,而是先将数据刷新到操作系统缓冲区,然后操作系统再将数据刷新到磁盘或者显示器上。(操作系统有自己的刷新机制,我们不需要关心它)。
我们可以看下图的层状结构图,用户区的数据要到显示器必须要经过操作系统,因为操作系统时对软硬件管理的软件。
理解文件系统
初识inode
磁盘文件由两部分构成,分别是文件内容和文件属性,文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,比如文件名、文件大小和文件创建时间等信息都是文件属性,文件属性又被称为元信息。
我们可以输入 ls -l 来查看当前目录下各文件的属性信息。
其中,各列信息对应的文件属性如下:
在Linux系统中,文件的元信息和内容是分开存储的,保存元信息的结构为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,也就是inode号。所以inode时一个文件的属性集合,Linux中几乎每个文件都有一个inode,又因为系统有大量的inode,所以为每个inode都设置了一个inode号
我们可以输入 ls -i 来查看每个文件的innode号:
当然,无论是文件内容还是文件属性,都是存储在磁盘当中的。
EXT2文件系统的存储方案
计算机为了更好地管理磁盘,会对磁盘进行分区,对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),剩下的分区会被EXT2文件系统根据分区的大小将其划分为一个个的块组(Block Group)。
1.启动块的大小是由格式化时确定的,不可更改。
2.每个组块有相同的组成结构,都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位表(Block Bitmap)、inode位表(inode Bitmap)、inode表(inode Table)和数据表(Data Block)组成
1.Super Block:存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息、Super Block的信息被破坏,可以说整个文件系统结构被破坏了。
2.Group Descriptor Table:块组描述符表,描述该分区当中块组的属性信息。
3.Block Bitmap Table:块位图记录着Data Block中数据块的占用情况。
4.inode Bitmap:inode位图当中记录着每个inode是否空闲可用。
5.inode Table:存放文件属性,即每个文件的inode。
6.Data Blocks:存放文件内容。
我们可以从创建一个空文件开始理解整个过程:
1.创建一个空文件:
1.通过遍历inode位图找到空闲的inode
2.在inode表找到对应的inode,将文件属性信息填入inode结构
3.将该文件的文件名和inode指针添加到目录文件的数据块中
2.对空文件写入信息:
1.通过文件的inode编号找到对应的inode结构
2.通过inode结构找到存储该文件内容的数据块,将数据写入数据块。
3.如果不存在数据块或者申请的数据块已经被写满,那么会遍历块位图寻找一个空闲的块号,然后再在数据去当中找到对应的空闲块,再继续写入数据,最后建立数据块与inode结构的对应关系
一个文件使用的数据块和inode结构的对应关系是通过一个数组进行维护的,这个数组可以存储15个元素,前12个元素对应着文件使用的12个数据块,剩下3个元素分别时一级索引、二级索引、三级索引,当数据块超过12个的时候,可以使用这三个索引进行数据块扩充。
3.删除一个文件:
1.将文件对应的inode在inode位图中置为无效
2.将该文件申请过的数据块在块位图中置为无
ln -s
效
因为这个删除操作实际上没有删除数据,只是将inode号和数据块置为无效,所以删除操作后短期内是可以恢复的。
一旦后续创建新的文件或者申请inode号和数据块号将其覆盖之后,那么就无法恢复了。
如何理解目录
1.目录也是文件
2.目录有着自己的属性信息,也有inode结构
3.目录也有自己的内容,也就是目录下文件的文件名和文件inode指针
需要注意的是:文件的文件名没有存储在自己的inode结构,而是存储在目录文件的文件内容。
软硬链接
软链接
在Linux中,我们可以使用以下命令来创建一个文件的软链接:
ln -s
例如:
可以发现,这里有一个新的link-s是指向link文件的。
我们再通过 ls -i -l 命令:
我们还可以看到:link和link-s的inode号是不同的,而且link-s的文件大小比link小非常多。
所以软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,有着自己的inode号,这个文件只包含了源文件的路径名,所以软链接比源文件小很多,类似于windows中的快捷方式。
需要注意的是:源文件一旦被删除,那么软链接文件就不能运行了。因为软链接文件是指向源文件的。
硬链接
我们可以使用下列命令来创建一个文件的硬链接:
ln
例如下列命令:
此时可以用 ls -i -l 命令来查看inode号:
可以发现,此时link-h并没有指向link,而且大小是和link一样,inode号也是一样,但是硬链接数变成了2,说明说明这个数据有两个硬链接,分别是源文件和硬链接文件。
所以我们可以知道,硬链接文件可以看成是源文件的别名。
需要注意的是:源文件链接删除之后,硬链接还是可以执行,只是链接数少了一个。
软硬链接的区别
1.软链接是一个独立的文件,有独立的inode,但是硬链接没有独立的inode
2.软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和inode的映射关系,然后将其写入目录
文件的三个时间
在Linux中,我们可以使用 stat 来查看文件信息:
Access:文件最后被访问的时间
Modify:文件最后的修改时间。
Change:文件属性最后的修改时间
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
另外,我们可以使用 touch 命令来将这三个时间信息更新到最新状态。