文章目录
Linux基础IO
前言
我们之前都有学过文件操作相关的函数,能够利用C语言相关的库函数进行文件的写入和读取;我们只是会用相关的库函数接口,但是并不知道文件究竟是怎么被写入的,怎么被读取的,文件操作的底层原理究竟是什么我们一概不知,接下来将会详细介绍文件操作的底层原理。让我们对文件操作有一个新的认识。
一、复习C文件的IO相关操作
1.C语言中相关的文件操作接口介绍
函数接口 | 函数说明 |
---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 一次写一个字符 |
fgetc | 一次读一个字符 |
fputs | 写一行数据 |
fgets | 读一行数据 |
fprintf | 格式化输出函数 |
fscanf | 格式化输入函数 |
fwrite | 以二进制的形式将数据写入 |
fread | 以二进制的形式将数据读出来 |
fseek | 根据文件指针的位置和偏移量来定位文件指针 |
ftell | 计算文件指针相对于起始位置的偏移量 |
rewind | 让文件指针回到起始位置 |
feof | 判断是不是遇到文件末尾而结束的 |
ferror | 判断是不是遇到错误后读取结束 |
2.C语言中的写文件
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
int main(){
//打开当前目录下的log.txt文件,以写的方式打开,如果没有该文件就会创建
FILE* fp=fopen("./log.txt","w");
if(NULL==fp){
perror("fopen");
return 1;
}
int count=5;
while(count--){
const char* message="hello linux\n";
fputs(message,fp);//向文件中写入字符串
}
fclose(fp);//关闭文件
return 0;
}
3.C语言中的读文件
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
int main(){
FILE* fp = fopen("./log.txt","r");
if(NULL==fp){
perror("fopen");
return 1;
}
char message[128];
int count=5;
while(count--){
fgets(message,sizeof(message),fp);
printf("%s",message);
}
fclose(fp);
return 0;
}
4.C程序默认打开的三个输入输出流
以上我们简单演示了C语言的文件操作,但这只是停留在语言层面上,简单的会使用还是不够的,很难对文件有一个比较深刻的理解。我们都知道C程序会打开三个默认输入输出流:
extern FILE *stdin; //标准输入 --- 所对应的是键盘
extern FILE *stdout; //标准输出 --- 所对应的是显示器
extern FILE *stderr; //标准错误 --- 所对应的是显示器
我们刚刚在使用fputs函数向文件写入数据,而文件的类型和这里默认打开的三个流的类型是一样的,那么我们也可以直接向显示器写入数据:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
int main(){
const char* msg="hello world\n";
fputs(msg,stdout);
return 0;
}
fputs能够像一般文件或者是硬件写入数据,我们就可以理解为在Linux操作系统中一切皆文件;
二、系统文件IO
我们无论是写文件还是读文件,文件都是来源于硬件(磁盘…),硬件是由操作系统管理的;用户不能够直接跳过操作系统将文件写入,必须贯穿整个操作系统。那么访问操作系统就需要调用系统接口来实现文件像硬件写入的操作;也就是你在C语言或其他语言上使用的文件相关库函数,其底层都是要调用系统接口的;如下图所示:
1.open接口
系统接口使用open函数打开文件,其需要的头文件有三个
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
1.open的参数
- pathname:你要打开的文件路径,如果没有会自动创建
- flags:以什么样的形式打开:
- O_RDONLY ——以只读的方式打开文件
- O_WRNOLY ——以只写的方式打开文件
- O_APPEND ——以追加的方法打开文件
- O_RDWR ——以读写的方法打开文件
- O_CREAT ——当目标文件不存在时,创建文件
- mode:表示创建文件时的权限设置
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
int fd=open("log.txt",O_WRONLY | O_CREAT,0644);
if(fd<0){
printf("open error\n");
}
close(fd);
return 0;
}
在上面代码中的第二个参数需要解释一下,我们刚刚已经了解了第二个参数是以什么样的形式打开文件,它是操作系统在用户层面上给内核传递的标志位。flags的类型是int,他就有32个比特位,一个比特位就可以代表一个标志位,如果两个或者多个进行或运算那么就可以传递多个标志位。操作系统内部在进行按位与运算,判断哪个位被设置了1或0,从而对文件打开方式进行设置。
实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
路径 /usr/include/bits/fcntl-linux.h
2.open的返回值
open的返回值是fd——文件描述符(文件打开成功,返回对应的fd值,打开失败,返回的是-1)
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
int fd1=open("log1.txt",O_WRONLY|O_CREAT,0644);
int fd2=open("log2.txt",O_WRONLY|O_CREAT,0644);
int fd3=open("log3.txt",O_WRONLY|O_CREAT,0644);
int fd4=open("log4.txt",O_WRONLY|O_CREAT,0644);
printf("fd1 = %d\n",fd1);
printf("fd2 = %d\n",fd2);
printf("fd3 = %d\n",fd3);
printf("fd4 = %d\n",fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
从上图的运行结果发现,文件描述符从3开始,依次递增,有点像数组的下标;那么0/1/2是什么呢?这里没有从0开始,其实,是将0/1/2分配给了三个流:0——标准输入、1——标准输出、2——标准错误
2.close接口
关闭文件描述符:int close(int fd);
关闭文件只需要传入你想要关闭的文件描述符即可;
3.write接口
向文件描述符写入数据:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数解读:将buf中的数据写入fd,写入count个
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
int fd=open("log1.txt",O_WRONLY|O_CREAT,0644);
if(fd<0){
perror("open");
return 1;
}
const char* msg="hello linux!\n";
int count=5;
while(count--){
write(fd,msg,strlen(msg));
}
close(fd);
return 0;
}
4.read接口
从文件描述符读数据:ssize_t read(int fd, void *buf, size_t count);
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
int fd=open("./log1.txt",O_RDONLY);
if(fd<0){
perror("open");
return -1;
}
char buffer[128];
ssize_t ss=read(fd,buffer,sizeof(buffer)-1);
if(ss>0){
buffer[ss]=0;
}
close(fd);
printf("%s\n",buffer);
return 0;
}
三、文件描述符fd
一个进程可以打开多个文件,当操作系统中存在大量的文件时,操作系统就需要对这些文件进行管理,在内核当中,管理这些已经打开的文件就要设计对应的结构体,把打开的文件描述起来,我们把描述文件的结构体称为(struct file),然后将这些结构体以双链表的形式链接起来,便于管理。
多个进程和多个文件在操作系统中,是如何区分哪一个文件属于哪一个进程的呢?
操作系统为了能够让进程和文件之间产生关系,进程在内核当中包含了一个结构struct file_struct,这个结构又包含了一个数组结构struct file* fd_array[] 在task_struct的PCB当中又包含了一个指针struct files_struct* fs,用来管理这个struct file_struct;我们把对应的描述文件结构的地址写到特定的下标里。所以为什么我们在打印文件描述符fd时是从3开始的,是因为前面3个地址留给了三个流,当有新的文件打开时,首先是形成了struct files结构体,然后将地址写入下标3的位置。然后返回给上层用户,我们就拿到了3这个下标了。
当我们在使用write和read时,都需要传入fd,本质上就是去进程的PCB中找到fd所对应的文件,就可以对文件进行操作了;结论:fd本质是内核中进程和文件相关联的数组下标
四、一切皆文件
对于我们的外设(IO设备),在驱动层一定对应了相应的驱动程序,包括他们各自的读写方法;他们的读写方法是不一样的;
在操作系统层面上,对于底层的键盘、显示器、磁盘等外设,需要打开时,操作系统就会给这些外设创建一个struct file的结构体进行维护,这些结构体就包含了相关外设的属性信息,再将它们用双链表管理起来。再与上层的进程结合起来即可以执行对应的操作了。这里就是所谓的虚拟文件系统(VFS)
我们在C++的学习中,对多态的概念有所了解,就是多个子类继承了相同的父类,每个子类的方法都是不一样的,只要父类的指针或引用调用对应的子类,就去实现对应子类的方法。在C语言中,想要实现多态,我们的方法是通过函数指针;
在这个struct file的结构中,就包含了读写方法的函数指针,对应到了每个外设;在上层看来,所有的文件只要调用对应外设的读写方法即可,根本不关心你到底是什么文件。
本质上,所谓的一切皆文件,就是站在struct file的层面上看待的。
五、文件描述符的分配规则
我们在第二节:系统文件IO的open接口的返回值中已经观察到了连续打开4个文件,所对应的fd分别是3,4,5,6;从运行结果看,它是从3开始向上增长的,因为0/1/2被三个流所占用。
那,如果我们将0关闭,会是什么样的呢?在原来代码的基础上加上close(0);
我们再关闭close(2);
观察现象得到的结论是:给新文件分配fd时,是从fd_array数组中找一个最小的,没有被使用过的,作为新的fd。、
六、重定向
本来应该写到显示器的数据,却写到了文件中,我们把这种现象叫做重定向;
1.输出重定向
我们先看下面的代码,我们本意是想将hello linux! 打印到显示器上:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
close(1);
int fd=open("./log.txt",O_CREAT|O_WRONLY,0644);
printf("fd:%d\n",fd);
printf("hello linux\n");
printf("hello linux\n");
printf("hello linux\n");
printf("hello linux\n");
return 0;
}
我们只是加了一个close(1);这段代码为什么就打印到文件中了呢?
printf函数本质是向stdout输出数据的,而stdout是一个struct FILE*类型的指针,FILE是C语言层面上的结构体,该结构体当中有一个存储文件描述符fd,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
C语言的数据并不是立马写到操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
2.追加重定向
追加重定向和输出重定向的本质区别在于,前者不会覆盖原来的数据内容。
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
close(1);
int fd=open("./log.txt",O_CREAT|O_WRONLY|O_APPEND,0644);
printf("fd:%d\n",fd);
printf("hello linux\n");
printf("hello linux\n");
printf("hello linux\n");
printf("hello linux\n");
return 0;
}
3.输入重定向
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
close(0);
int fd=open("./log1.txt",O_RDONLY);
char buf[128];
while(~scanf("%s",buf)){
printf("%s\n",buf);
}
close(fd);
return 0;
}
4.stdout和stderr有什么区别呢?
标准输出流和标准错误流对应的都是显示器,它们有什么区别呢?
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
fprintf(stdout,"hello stdout!\n");
fprintf(stderr,"hello stderr!\n");
return 0;
}
我们同时向标准输出和标准错误中输出数据,都是能够打印到显示器上的;当我们将其重定向到文件中去时,却发现只有stdout的内容重定向到了文件中。实际上我们再使用重定向的时候,是把文件描述符1的标准输出流重定向了,而不会对标准错误流重定向。
七、系统调用dup2
以上的操作我们都是在关闭标准输入和标准输出后完成重定向,显得很麻烦,如果标准输入和标准输出都被占用(已经打开了),我们如何去完成重定向呢?要完成重定向我们就可以将fd_array数组中的元素进行拷贝即可;例如:我们将fd_array[3]中的内容拷贝到fd_array[1]中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。|
在linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。本质上dup2就是将进程中文件描述表中的需要重定向的内容进行相关的拷贝,dup2的函数原型如下:
#include<unistd.h>
int dup2(int oldfd, int newfd);
函数功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]中;
函数返回值:调用成功返回0,失败返回-1;
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并且返回newfd
使用dup2进行输出重定向:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
int fd=open("./log.txt",O_WRONLY);
if(fd<0){
perror("open");
return 1;
}
dup2(fd,1);//本来应该显示到显示器上的内容,被重定向到了文件中
printf("hello-world!\n");
printf("hello-linux!\n");
return 0;
}
使用dup2进行输入重定向:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
int fd=open("./log.txt",O_RDWR);
if(fd<0){
perror("open");
return 1;
}
dup2(fd,0);//本来应该从键盘上读取数据,改成了从文件读取数据
char str[128];
scanf("%s\n",str);
printf("%s\n",str);
return 0;
}
八、关于FILE
1.C库当中的FILE结构体
因为IO相关函数与系统调用接口对应,并且库函数封装了系统调用,所以本质上,访问文件都是通过fd来访问的。所以C库当中的FILE结构体内部,必定封装了fd。
我们可以使用vim打开/usr/include/stdio.h
的文件查看FILE
typedef struct _IO_FILE FILE; //在/usr/include/stdio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
2.FILE结构体中的缓冲区
从FILE的源码当中我们发现了FILE结构体里面封装了fd,也就是里面的_fileno。不难发现我们在里面还看到了缓冲区。这里的缓冲区指的是C语言当中的缓冲区。
1.初步了解缓冲区
我们先来看一下下面的代码,代码的含义是输出重定向,观察是否关闭文件描述符,对结果有何影响?
-
不关闭文件描述符,进行输出重定向
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c #include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main(){ close(1);//将标准输出关闭后,fprintf所打印的数据将会重定向到文件log.txt中 int fd=open("./log.txt",O_CREAT|O_WRONLY,0644); printf("fd: %d\n",fd); fprintf(stdout,"hello linux!\n"); fprintf(stdout,"hello linux!\n"); fprintf(stdout,"hello linux!\n"); fprintf(stdout,"hello linux!\n"); //close(fd);//不关闭fd return 0; }
-
关闭文件描述符,进行输出重定向
2.缓冲区的深入理解
通过上面两次的运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)的。在操作系统内部也是存在一个内核缓冲区的,用户缓冲区到内核缓冲区的刷新策略有如下几种:
- 立即刷新:不缓冲
- 行刷新(行缓冲\n),比如,显示器打印
- 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入
当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
显示器是行缓冲,即遇到’\n‘就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;
当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;两者都是通过系统调用接口(open)来完成数据的写入;在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;
再来看一下下面这段代码:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
const char* msg1="hello stdout\n";
write(1,msg1,strlen(msg1));
const char* msg2="hello stderr\n";
write(2,msg2,strlen(msg2));
printf("hello linux!\n");
fprintf(stdout,"hello fprintf!\n");
close(1);
return 0;
}
通过上面的运行结果,再结合之前所说,我们这里不是关闭了1吗?为什么还是能够打印出来呢?我们可以看到这里输出语句都是向显示器打印的,并且都有‘\n’,表明是行刷新,在关闭1之前就已经刷新到显示器上了。
标准错误不会重定向我们能够理解,但是其他三条语句应该是重定向文件中了呀,而这里运行结果只有一条hello stdout。这是因为我们的msg1是直接通过系统调用接口,把数据暂存到了内核缓冲区,不会把数据暂存到上层的用户级缓冲区,所以关闭1根本就不会影响这个数据刷新到文件;但是下面两个语句由于重定向的原因,刷新策略发生了变化(行缓冲->全缓冲),数据暂存到用户级缓冲区后,本来是等待进程结束后刷新到文件中去的,但是这个过程却把1关闭了,才导致这两条数据并没有被刷新到文件中;
对于以上的理解有了新的认识后,我们再来看一下这段代码:
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
const char* msg1="hello stdout\n";
write(1,msg1,strlen(msg1));
printf("hello linux!\n");
fprintf(stdout,"hello fprintf!\n");
fork();
return 0;
}
通过上面的运行结果发现,当我们直接运行程序,它向显示器上打印了3条语句,但是我们程序中有创建子进程的语句,当我们重定向后发现文件中多打印了两条语句,而且只是针对C语言的接口,而非系统调用接口;
当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当前每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就由行缓冲变成了全缓冲,此时我们使用printf和fprintf函数打印的数据都暂存到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fprintf函数打印的数据就有两份。
3.如何解决缓冲区不刷新的问题
[ljh@VM-8-16-centos 2024_2_5]$ cat myfile.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(){
close(1);//将标准输出关闭后,fprintf所打印的数据将会重定向到文件log.txt中
int fd=open("./log.txt",O_CREAT|O_WRONLY,0644);
printf("fd: %d\n",fd);
fprintf(stdout,"hello linux!\n");
fprintf(stdout,"hello linux!\n");
fprintf(stdout,"hello linux!\n");
fprintf(stdout,"hello linux!\n");
fflush(stdout);//强制刷新缓冲区
close(fd);//关闭fd
return 0;
}
九、理解文件系统及inode
如果一个文件没有被打开,它肯定是存放在磁盘当中的,刚刚是对文件进行了分析,接下来我们来谈谈磁盘文件。
1.磁盘的概念
我们所创建的大量文件,在没有被打开时都是在磁盘上存放的,以下是磁盘的基本结构图:
如下图所示,磁盘主要是通过机械臂上的磁头来读取磁盘上的数据,磁盘由一个或者多个圆盘组成,它们围绕着一根中心主轴旋转,磁盘被组织成磁道,磁道是单个盘片上的同心圆,所有盘面上半径相同的磁道构成了柱面,每一个磁道有按512字节为单位划分为等分,叫做扇区,向磁盘读取和写入数据时,要以扇区为单位。
2.磁盘的分区与格式化
1.磁盘的分区
磁盘由一个或者多个圆盘组成,我们可以把它想象成小时候都接触过的磁带,把磁盘展开成一条直线。(线性结构)磁盘是相当大的,如果对整个磁盘进行管理成本很高,那么就需要对磁盘进行分区操作。
2.磁盘的格式化
在Linux ext系列文件系统中,上面为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:block和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息;Block Bitmap中记录着Date Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- 数据区(Data block):存放文件内容
补充一点,以上我们是对文件进行了说明,我们知道一切皆文件,那么目录也是文件,只不过我们感官上觉得目录和文件有些区别;
目录既然也是文件,如何结合以上所将来解释创建一个目录呢?
当我们创建一个目录后,它也是有inode编号的,在block中存放的是文件的文件名,因为文件名也是字符串,字符串也就是数据,再通过文件的inode编号进行映射,就能访问到文件了。
总结:
创建一个新文件主要有以下4个操作:
-
存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中
-
存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800.将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
-
记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表
-
添加文件名到目录
新的文件名 test。linux如何在当前目录中记录这个文件?内核将入口(263466,test)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
3.查看inode信息
1.stat命令
stat命令用来显示文件或文件系统的详细信息,stat命令的输出信息要比ls命令的输出信息更加详细。
简单介绍以下stat命令显示出来的文件其他信息:
- File:显示文件名
- Size:显示文件大小
- Blocks:文件使用的数据块总数
- IO Block:IO块大小
- regular file:文件类型(常规文件)、
- Device:设备编号
- Inode:Inode号
- Links:链接数
- Access:链接数
- Gid、Uid:文件所有权的Gid和Uid(拥有者和所属组)
2.ls -al -i指令
查看文件的inode编号
4.Linux下的三个时间
- Access Time:简称为atime,表示文件的访问时间。当文件内容被访问时,更新这个时间(短时内访问不会更新)
- Modify Time:简称mtime,表示文件内容的修改时间,当文件的数据内容被修改时,更新这个时间。
- Change Time:简称为ctime,表示文件的状态时间,当文件的状态被修改时,更新这个时间,例如文件的链接数,大小,权限,Block数。
十、软硬链接
1.软链接
我们可以通过以下命令创建一个文件的软连接。
ln -s log.txt log_s //创建软连接
unlink log_s //删除软连接
创建这个软链接有什么用呢?
例如:你想要执行bin目录下的a.out程序
在Linux中有时候会存在执行路径特别深的程序,这时候我们就可通过软连接快速的找到;本质上就是充当索引的作用;并且两者的inode是不相同的,软连接是由独立的inode的,是个独立文件,可以跨文件系统;
需要注意,当我们完成软链接后如果删除链接的目标,软链接文件不会独立存在,虽然仍保留文件名:
2.硬链接
我们可以通过以下命令创建一个文件的硬链接
ln log1.txt hard
刚刚软连接我们将链接的目标删除后,是不能独立存在的;如果我们把硬链接目标删除后会发生什么情况呢?
从上图可以看到,删除myfile.c后,查看我们的硬链接文件,依旧能够看到内容。这里就相当于给log.txt重命名了。
我们发现硬链接的inode和源文件的inode是相同的,并且链接数是2,而软连接的连接数是1;
这里的链接数表示的是硬链接数;
硬链接的本质是根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为字节没有独立的inode
硬链接的另一种场景:
我们创建一个目录后,硬链接数就是2,其原因就是一个inode对应了两个文件名(test和.)