#include<stdio.h
int main()
{
FILE * fp=fopen("./log.txt","w");//"a",appand 追加写入
if(NULL==fp) {
perror("OPEN"); return 1;
}
int cnt=10;
const char* str="ahah\n";
while(cnt--)
{
fputs(str,fp);
}
fclose(fp);
return 0;
}
#include<stdio.h
int main()
{
FILE * fp=fopen("./log.txt","r");
if(NULL==fp)
{
perror("OPEN");
return 1;
}
int cnt=10;
char buff[128];
while(fgets(buff,sizeof(buff),fp))
{
printf("%s\n",buff);
}
fclose(fp);
return 0;
}
#include<stdio.h
int main()
{
const char * msg="hello fwrite\n";
fwrite(msg,strlen(msg),1,stdout);
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
return 0;
}
所有的文件操作,表现上都是进程执行对应的函数!进程对文件的操作,对文件的操作就要先打开文件,打开文件的本质就是加载文件相关的属性,加载到内存!
为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?因为系统已经默认给我们打开了三个流了
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
''输出重定向的本质就是把stdout的内容重定向到文件中(stderr重定向时不能够吧内容重定向到文件中 可以证明)
如何理解?
最终都是访问硬件(显示器,硬盘,文件(磁盘),os是硬件的管理者,所有的语言上对“文件”的操作,都必须贯穿os!一切皆文件
所以,几乎所有的语言例如fopen,fgets,等,在底层一定需要使用os提供的系统调用!为了更好的使用文件操作,我们需要学习文件的系统调用接口
下面是c语言提供好的函数接口,本质就是对系统接口进行封装
文件操作函数 | 功能 |
---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int main()
{
//FILE* fd=fopen("./log.txt","w");等于下面
int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
if(fd<0){
printf("open error\n");
}
close(fd);
return 0;
}
参数的介绍:
1、pathname参数: 文件路径
2、flags参数: 传递标志位
int有32个bit,一个bit,代表一个标志,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。
O_WRONLY O_RDONLY O_CREAT等都是只有一个比特位是1的数据,而且不重复
所以我们传入多个标志,只需要给每个相邻的标志按位或即可
注意:实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
例如:
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
#define O_CREAT 0100
因为每个标志都是独立的,所以open内部就可以通过&按位与来进行区分选项,
例如:
int open(arg1, arg2, arg3){
if (arg2&O_RDONLY){
//设置了O_RDONLY选项
}
if (arg2&O_WRONLY){
//设置了O_WRONLY选项
}
if (arg2&O_RDWR){
//设置了O_RDWR选项
}
if (arg2&O_CREAT){
//设置了O_CREAT选项
}
//...
}
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRONLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
3、mode_t mode参数: 打开权限
例如 将mode设置为:0666,则创建出来的文件权限为: -rw-rw-rw-
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
注意: 当不需要创建文件时,open的第三个参数可以不必设置
4、返回值: 成功:新打开的文件描述符(如果对文件描述符不理解的可以看下面文件描述符详解)
失败:-1
由下面显示:
int main()
{
int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
int fd2=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
int fd3=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
int fd4=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
if(fd1<0){
printf("open error\n");
}
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
return 0;
}
//打印
3
4
5
6
其中:0 1 2,代表标准输入,标准输出,标准错误
0123456 文件描述符本质上是一个指针数组的下标--------
所有的文件操作,表现上都是进程执行对应的函数!进程对文件的操作,对文件的操作就要先打开文件,打开文件的本质就是加载文件相关的属性,加载到内存!
操作系统中存在大量的进程,进程对文件的比例是 1:n,那么系统中就存在可能更多的,打开文件!打开文件是加载到内存中,os必须对文件进行管理,先描述后组织,
struct file{
//包含了打开文件的相关属性
// 打开文件之间的链接属性
}
系统接口中使用close函数关闭文件,close函数的函数原型如下:
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
头文件#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
函数说明:write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。
三个参数:
第一个参数 文件描述符fd
第二个参数 无类型的指针buf,可以存放要写的内容
第三个参数 写多少字节数
strlen()用来读取长度
#include <stdio.h
#include <string.h
#include <unistd.h
#include <sys/types.h6
#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";
for (int i = 0; i < 5; i++){
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
头文件 :#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read函数的三个参数:
(1)fd:文件描述符
(2)buf:指定读入数据的数据缓冲区
(3)count:指定读入的字节数
返回值:
成功:返回读取的字节数
出错:返回-1并设置errno
如果在调read之前已到达文件末尾,则这次read返回0。
我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
int fd = open("./log.txt", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
char buffer[1024];
ssize_t s = read(fd, buffer, sizeof(buffer)-1);
if(s > 0){
buffer[s] = 0;
printf("%s\n", buffer);
}
通过对open函数的学习,我们知道了文件描述符就是一个小整数
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件 。
补充:那么文件是什么时候关闭的,准确的来说file文件对象什么时候被移除,这里会涉及到引用计数器;
磁盘文件vs内存文件
类似于程序与进程的关系,当程序被运行时,先创建进程PCB,mm_struct,页表等系统级的结构体,加载相关的数据和代码到内存中,通过页表对虚拟内存与物理内存物理产生映射关系;
磁盘文件与内存文件,也是一样的,打开的文件都会有一个对象,用来保存相关的文件属性,文件对象间的链接关系类似于双链表,在要读取文件等操作时,才会把文件数据加载到物理内存中
通过一下实验进行证明:
通过上面的一下样例,0,1,2文件描述符对应标准输入输出错误,现在我们看看关闭文件 0,然后再打开一个文件,我们都知道如果不关闭0那么新打开的文件的文件描述符为3。
int main()
{
close(0);
int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
if(fd1<0){
printf("open error\n");
}
printf("%d\n",fd1);
return 0;
}
//打印
0
close(0) ,file*fd_array[0]里的指针不指向可用文件,也就是说array[0]没有被使用,指向新打开的./log.txt文件对象的指针存放到array[]里,通过文件描述符的分配规则把指针存放到相应的下标里,并且返回该文件描述符。
文件描述符的分配规则:在files_struct里的file* fd_array[]数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
重定向原理:基于文件描述符的分配规则;
#include <stdio.h
#include <unistd.h
#include <sys/types.h
#include <sys/stat.h
#include <fcntl.h
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open failed");
return 1;
}
printf("hello\n");
fflush(stdout);
close(fd);
return 0;
}
原本我们的printf要把"hello\n" 输出到显示器上的,现在确输出到log.txt的文件里;
其实就是把文件描述符1 里的内容不指向任何一个struct file,然后打开文件时,通过文件描述符分配规则进行分配;
现在文件描述符1里就放了新打开的文件指针
追加重定向也是类似
在上述上,打开文件时按照追加的形式打开即可;
#include <stdio.h
#include <unistd.h
#include <sys/types.h
#include <sys/stat.h
#include <fcntl.h
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
if (fd < 0){
perror("open failed");
return 1;
}
printf("hello\n");
fflush(stdout);
close(fd);
return 0;
}
需要注意的是:
1.printf() 是C语言的库函数,对open()进行封装,printf()想要访问文件必须有文件描述符,printf()默认向stdout输入数据,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。类似scanf()函数也是如此,stdin 指向的结构体,该结构体当中有一个变量存储文件描述符0;
2.上述重定向的代码都是对标准输出进行重定向,如果想对stderr重定向,只需要close(2),然后打开重定向后的文件。
我们想要把输出到显示器,重定向到log.txt文件里,那么我们只需要把file * fd_array[3] 的内容复制到file * fd_array[1]里即可;
操作系统给我们提供了dup2接口,我们可以使用它完成重定向。
int dup2(int oldfd, int newfd);
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
//如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
//如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
#include <stdio.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_APPEND|O_CREAT, 0666);
if (fd < 0){
perror("open failed");
return 1;
}
close(1);
dup2(fd,1);
printf("hello dup2\n");
fflush(stdout);
close(fd);
return 0;
}
上述代码用途为输出重定向,open打开文件log.txt返回fd指针,关闭fd 1(标准输出),把 fd_array[fd] 复制到fd_array[1]里;printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1
下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向 。
dup系列详解
linux 关闭打开的文件描述符,关闭它们后,如何重新打开stdout和stdin文件描述符?
详解
上述我们知道了write,open,read这些都是系统接口,在C语言里fprintf,等函数对这些系统接口进行封装,其中在C语言上有stdout,stdin,stderr流,其实就是一个FILE*的指针,调用printf时其实就是向stdout指向的 struct _IO_FILE 这个结构体里写入,这个结构体包含了fd,用户级缓存区;printf把数据写入到c缓冲区里,printf就完成任务了,然后C语言的缓存区按照刷新规则刷新到操作系统的缓冲区里这里的刷新也需要调用系统接口,也需要fd,然后再按照操作系统的刷新机制进行对硬件的写入;下面来详细介绍
缓冲区的作用如果你了解可以不点进来
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
};
我们看到的 int _fileno成员变量;就是封装后的文件描述符fd;
首先来段代码研究一下:
#include <stdio.h
#include <string.h
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行结果:
hello printf hello fwrite hello write
如果输出重定向到一个文件时(./hellofile):
文件内容:
hello write hello printf hello fwrite hello printf hello fwrite
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲
c语言缓存区刷新机制:
行缓存按行刷新
全缓冲按满了就刷新,或者进程退出的时候,会刷新FILE内部的数据到os缓冲区
该缓冲区由C语言提供,该缓冲区的位置在FILE结构体中,也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。
在来看几个例子:
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
return 0;
}
// 向屏幕输出
/*
hello printf
hello fwrite
hello write
*/
int main()
{
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
close(1);
dup2(fd,1);
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
return 0;
}
// 重定向输出
// 把内容输入到log.txt文件里;
int main()
{
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
close(1);
dup2(fd,1);
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
close(1);
return 0;
}
//只把write的内容输入到log.txt文件里
//这里就因为C语言的缓冲区没有及时的刷新
//最后把fd 1 关闭了,自然的在刷新到fd:1对应的文件了;
//解决:在关闭前 刷新,可以调用fflush(stdout);
//缓冲区相关
/* 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. */
补充:
操作系统也有缓存区
当我们刷新用户缓冲区(例如c语言缓冲区)时,并不是立刻将数据刷新到磁盘或显示器上,而是先将数据缓存到操作系统的缓存区里,然后操作系统的缓存区等待操作系统自制的刷新机制进行刷新。这一方面我们没必要过多的了解,只需知道操作系统也是有缓冲区的;
内存文件是如何管理的上述已经介绍了,下面我们来理解一下文件系统是如何管理磁盘文件的。
如果一个文件没有被打开
文件=文件内容+文件属性;文件放在磁盘上
文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小创建时间等信息都是文件属性,文件属性又称为元信息。
我们使用ls -l
的时候看到的除了看到文件名,还看到了文件元数据 。
每行包含7列 :
ls -l读取存储在磁盘上的文件信息,然后显示出来
在Linux里,文件的元信息和内容是分离的,保存元信息由一个inode的结构保存,在操作系统里存在大量的inode结构,所以我们需要给每个inode进行编号,即inode 号。在os里几乎所有的文件都有一个唯一标识的inode。
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
File: ‘abc’
Size: 9 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 787165 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ BBQ) Gid: ( 1003/ BBQ)
Access: 2022-04-21 18:50:53.336108679 +0800
Modify: 2022-04-21 18:50:51.406039972 +0800
Change: 2022-04-21 18:50:51.409040079 +0800
Birth: -
把缓冲区数据刷新到磁盘实际就是os把数据写入到盘片上
什么是磁盘?
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
盘片—扇区: 盘片被分成许多扇形的区域
磁道:盘片以盘片中心为圆心,不同半径的同心圆。
**柱面:**硬盘中,不同盘片相同半径的磁道所组成的圆柱。
每个磁盘有两个面,每个面都有一个磁头。
磁盘查询方案:
磁盘的读写时是如何查询读写位置的?
盘面(磁头)---- 磁道 ---- 扇区
注意:磁盘写入的基本单位是:扇区------512字节
理解文件系统,我们必须将磁盘盘片想象成一个线性的存储介质,例如磁带,当磁道卷起来时就像磁盘一样是圆形的。
站在os角度,我们认为磁盘是线性结构的。如图:
LBA索引相当于虚拟地址与物理内存的关系,LBA是站在os角度认识的,最后LBA要转换成磁盘能读懂,磁盘查询步骤–确定盘面,柱面,扇区。
os系统为了更好的管理磁盘,与是就对磁盘进行分区管理。
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:
ls /dev/vda* -l
当磁盘完成分区后,我们还需要对磁盘进行格式化。格式化的本质就写入文件系统。
其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
上述了解磁盘的基本概念,主要目的是为了把磁盘想象成线性的结构,为了更好的管理,我们给磁盘进行分区细分化,然后再给每个分区格式化—配上管理系统。接下来我们要学习 一个没有被打开的文件是如何是磁盘是存储的。
os为了更好的管理磁盘,对磁盘进行分区,如果os把一个分区管理好了,实际上其他分区也可以按照同样的方法进行管理。如果你想让不同的分区按不同的文件系统进行管理,是可以的,因为现在所有的操作系统都支持多文件系统。
而对于每一个分区,分区的头部会有一个启动块(Boot Block),对于其他区域,EXT2文件系统会根据系统分区的大小将其划分为一个个的块组(Block Group)。
其次,每个块组都包含一个相同的结构,这个结构都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
重点学习后4个
我们可以把inode看成一个结构体,结构体里大概存放了
struct inode{
//文件的所有属性
// 数据 int inode_number;
int blocks[32];//数据块列表
int ref ;// 硬链接数量
}
如何理解位图?
假设二进制的位图:0000 1010
从左往右比特位的位置含义: inode编号
比特位的内容含义:特定inode“是否" 被占用
例如编号为1的位置为0,0代表没有被占用;
注意:上面理解位图,inode结构,都是方便大家理解。
注意:
如何理解创建一个文件?
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作 。
touch abc
ls -i abc
为了说明问题,我们将指令输出简化:
创建一个新文件主要有一下4个操作:
存储属性
内核通过inode Bitmap先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
存储数据
该文件需要存储在三个磁盘块,内核通过iBlock Bitmap找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。
记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
如何理解删除一个文件?
1.将文件对应的inode在inode位图当中置位无效。
2.数据块也同样如此,将该文件申请过的数据块在Block Bitmap当中置位无效。
所以,删除一个文件,不是把文件的内容删掉。如果我们误删一个文件,是可以恢复的。如果进行其他操作例如开辟一个文件,那么置为无效的文件可能被别人使用了,并修改了文件内容。所以有可能不能恢复。
如何理解目录?
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
ln abc def
硬链接本质是根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode。
创建硬链接,本质是在特定的目录下,填写一对文件名和inode 的映射关系。
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
ln -s abc abcs
软链接是有自己独立的inode的!软链接是一个独立文件!!!有自己的inode属性,也有自己的数据块(保存的是指向文件的所在路径+文件名)
下面解释一下文件的三个时间 :
Access:最后访问的时间 (较新的linux会在一定间隔的时间内进行更新,也就是说Access时间不会被立即更新,原因该时间高频被访问会影响性能)
Modify :文件内容最后修改时间
Change : 属性最后修改时间
注意:通常文件内容被修改后,不仅Modify被修改了,Change也会被修改,例如文件属性的大小被修改了。
touch file
touch 指令更新已存在文件的三个时间。
makefile与gcc会根据时间问题,来判定源文件和可执行程序谁更新,从而指导系统那些源文件需要被重写编译。