[Linux]:文件(下)
✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:Linux学习
贝蒂的主页:Betty’s blog
1. 重定向原理
在明确了文件描述符的概念及其分配规则后,我们就可以解释我们之前所说的重定向的原理。
1.1 输出重定向
输出重定向的本质就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中,即关闭对应标准输出流的文件描述符1,然后让该文件描述符重新指向新的文件,最后如果我们再对该文件描述符进行写入,本应该打印在屏幕的数据就重定向进入新文件。
同样该操作我们也可以通过代码实现:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(1);//关闭标准输出流
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open fail:");
return 1;
}
//向屏幕打印信息
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
fflush(stdout);//刷新缓冲区
close(fd);
return 0;
}
从上述输出我们发现,本应该向屏幕打印的数据,结果通过输出重定向打印进了log.txt
文件。
其中关闭文件之前我们必须刷新缓冲区,至于为什么,我们在后面讲解缓冲区时专门讲解。
1.2 输入重定向
输入重定向的本质也是与输出重定向同理,将我们本应该输入到一个文件的数据重定向输入到另一个文件中,即关闭对应标准输出流的文件描述符0,然后让该文件描述符重新指向新的文件,最后如果我们再对该文件描述符进行读取,本应该从键盘读取的数据就重定向变为从新文件读取。
同样该操作我们也可以通过代码实现:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(0);//关闭标准输入流
int fd=open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open fail:");
return 1;
}
//向屏幕打印信息
char buf[128]={'\0'};
while (scanf("%s", buf) != EOF)
{
printf("%s\n", buf);
}
close(fd);
return 0;
}
从上述输出我们发现,本应该向屏幕读取数据,结果通过输入重定向从log.txt
文件读取。
1.3 追加重定向
追加重定向本质就简单的多,只需要写入文件时加入O_APPEND
选项即可。
int fd=open("log.txt",O_WRONLY|O_APPEND|O_CREAT,0666);
然后我们可能疑惑的是,标准输出流与标准错误流对应的设备都是显示器,那么这两者之间有什么区别呢?
我们可以先看一下这段代码:
#include<stdio.h>
int main()
{
printf("stdout:hello printf!\n");
perror("stderr:hello perror!");
fprintf(stdout,"stdout:hello fprintf!\n");
fprintf(stderr,"stderr:hello fprintf!\n");
return 0;
}
如果直接运行的话,肯定会全部打印。但是如果对该执行文件进行输出重定向的话,标准错误流的文件内容就不会重定向进新文件中。
这是因为输出重定向默认关闭的是1号文件描述符,并没有关闭2号文件描述符。利用这一特性,我们就可以以后将错误信息单独提前出来,打印到日志系统中。
当然如果想将标准输出与标准输出的内容输出到同一文件中,也可以使用类似的指令。
1.4 dup2函数
其中Linux
操作系统也为了我们提供了专门的重定向接口——dup2
函数
- 原型:
- 函数功能:
dup2
会将fd_array[oldfd]
的内容拷贝到fd_array[newfd]
当中,如果有必要的话我们需要先关闭文件描述符为newfd
的文件。- 函数返回值: 如果调用成功,返回
newfd
,否则返回-1。
使用dup2函数
时,需要注意以下两点:
- 如果
oldfd
不是有效的文件描述符,dup2
就会调用失败,此时文件描述符为newfd
的文件没有被关闭。- 如果
oldfd
是一个有效的文件描述符,但是newfd
和oldfd
具有相同的值,则dup2
不做任何操作,并返回newfd
。
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open fail:");
return 1;
}
close(1);
dup2(fd,1);//进行重定向
printf("hello printf!\n");
fprintf(stdout,"hello fprintf!\n");
close(fd);
return 0;
}
2. 缓冲区
2.1 语言缓冲区
在计算机领域,缓冲区是一块存储区域。它用于暂存数据,以协调不同速度的设备或操作之间的数据传输。比如我们再来看看下面这段代码:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
close(1);//关闭标准输出流
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open fail:");
return 1;
}
//向屏幕打印信息
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
printf("hello betty!\n");
close(fd);
return 0;
}
为什么没有打印信息呢?其实这就与我们C语言的缓冲区有关,因为缓冲区常见的刷新策略有三种:
- 无缓冲。
- 行缓冲。(常见的对显示器进行刷新数据)
- 全缓冲。(常见的对磁盘文件写入数据)
其中对于我们的printf
函数,如果没有加\n
就是全缓冲,否则就是行缓冲。
因为我们对文件进行了重定向,让本应该向屏幕打印的信息输入进一个磁盘文件,这时缓冲策略就从行缓冲变成了全缓冲,全缓冲需要程序结束之后才会向磁盘刷新文件内容,但是在此之前文件我们已经调用close
接口关闭了对于的文件描述符,此时程序结束后就无法找到对应的文件,自然也不会对文件进行任何写入。所以一般为了解决这个问题,我们可以使用fflush
函数提前刷新缓冲区。
由于我们使用的printf
是C语言提供的接口,所以这个缓冲区也是C语言提供的,其被包含在名为File
的结构体中,不光是缓冲区,文件描述符fd
也被包含在其中。这也是为什么C语言的文件接口需要返回File*
的原因。
//在/usr/include/libio.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.2 系统缓冲区
不仅是我们语言方面存在缓冲区,我们操作系统内部也会存在一个缓冲区,我们一般称为内核缓冲区。同样语言缓冲区刷新到系统缓冲区也遵循三种刷新策略:
- 无缓冲。
- 行缓冲。(常见的对显示器进行刷新数据)
- 全缓冲。(常见的对磁盘文件写入数据)
所以说我们使用语言所提供的接口如printf
对文件进行写入数据,首先会将数据存放在语言缓冲区,然后根据不同的刷新规则再刷新到系统缓冲区中,最后才会将系统缓冲区的数据刷新到磁盘或者对应的外设之中。
比如说我们看看下面这段代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
//c
printf("hello printf\n");
fputs("hello fputs\n", stdout);
//system
write(1, "hello write\n", 12);
fork();
return 0;
}
为什么重定向之后的内容会与之前截然不同呢?
这是因为我们执行可执行程序,打印到屏幕,默认是行缓冲,所以直接打印所以数据。但是如果我们对数据进行重定向的话,向磁盘写入数据,默认为全缓冲,此时数据都会存在语言缓冲区中。而此时我们创建子进程,父子进程之间代码数据共享,进程结束之后对语言缓冲区进行刷新,本质就是对数据进行修改,为了进程之间的独立性,就会发生写实拷贝,所以重定向之后C语言接口的数据打印会答应两份。而因为系统接口
write
写入的数据是直接写入系统缓冲区的,不需要发生写实拷贝,所以只打印一份。
3. 理解文件系统
前面我们谈论的文件都是加载进内存的内存文件,而接下来我们就来谈谈磁盘文件。
3.1 磁盘
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,但是内存是掉电易失存储介质,所以目前所有的普通文件都是在磁盘中存储的。
了解磁盘,我们首先需要了解一下几个常见的基本概念。
如果我们需要确定磁盘的哪个特定的区域,只需要找到对应的扇区,磁道,柱面即可。
3.2 磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。为了方便描述,我们可以将磁盘抽象为线性存储。
在计算机中,为实现高效磁盘管理,常进行分区操作。分区编辑器可在磁盘上划分出多个逻辑部分,如 Windows
下常见的C
盘和D
盘。分区越多,文件管理越精细,不同性质文件可存储于不同分区。这样能更好地组织和管理文件,提升系统运行效率,便于用户快速找到所需文件,优化计算机使用体验。
在Linux
操作系统中,我们也可以通过指令ll /dev/vda*
查看我们磁盘的分区信息。
3.3 磁盘格式化
磁盘格式化是在磁盘完成分区后进行的一项重要操作。从本质上来说,它是对磁盘中的分区进行初始化,旨在为数据存储和管理建立一个规范的基础结构。
其中,初始化会写入相应的管理信息,这些管理信息是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2
、EXT3
、XFS
、NTFS
等。
需要强调的是,磁盘格式化具有一定的风险性。在进行这一操作时,通常会导致现有的磁盘或分区中所有的文件被完全清除,且这种清除往往是不可逆的。因此,在决定进行磁盘格式化之前,必须谨慎考虑并确保已对重要文件进行了妥善的备份。
3.4 inode
在Linux
操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode
,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode
编号。
其中我们可以通过指令ls -i
,显示当前目录下各文件的inode
编号。
在Linux
下,文件是通过innode
标识的,所以在系统层面文件名,后缀都是没有意义的。
3.5 EXT2文件系统的存储方案
对于每一个分区来说,分区的头部会包括一个启动块(Boot Block
),对于该分区的其余区域,EXT2
文件系统会根据分区的大小将其划分为一个个的块组(Block Group
)。其中启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
并且,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block
)、块组描述符表(Group Descriptor Table
)、块位图(Block Bitmap
)、inode
位图(inode Bitmap
)、inode
表(inode Table
)以及数据表(Data Block
)组成。
Super Block
: 存放文件系统本身的结构信息。记录的信息主要有:Data Block
和inode
的总量、未使用的Data Block
和inode
的数量、一个Data Blocks
和inode
的大小、最近一次挂载的时间、等其他文件系统的相关信息。Super Block
的信息被破坏,可以说整个文件系统结构就被破坏了。Group Descriptor Table
: 块组描述符表,描述该分区当中块组的属性信息。Block Bitmap
: 块位图当中记录着Data Block
中哪个数据块已经被占用,哪个数据块没有被占用。inode Bitmap
:inode
位图当中记录着每个inode
是否可用。inode Table
: 存放文件属性,即每个文件的inode
。Data Blocks
: 存放文件内容。
值得注意的是:
- 其他块组当中可能会存在冗余的
Super Block
,当某一Super Block
被破坏后可以通过其他Super Block
进行恢复。- 磁盘分区并格式化后,每个分区的
inode
个数就确定了。
一个文件使用的数据块和inode
结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。
通过上面的学习,我们就可以回答一下几个问题。
- 如何理解创建一个空文件?
- 遍历
inode Bitmap
,找到比特位为0的位置,申请一个未被使用的inode
。- 将
inode
表中找到对应的inode
, 并将文件的属性信息填到inode
结构当中。- 将该文件的文件名和
inode
指针添加到目录文件的数据块当中。
- 如何理解向文件写入信息?
- 通过文件的
inode
编号找到对应的inode
结构。- 通过
inode
结构找到存储该文件内容的数据块,并将数据写入数据块。- 若不存在数据块或者申请的数据块已经写满了,就需要遍历
block Bitmap
找到一个空的块号,并在数据区当中找到对应的空闲块,再把数据写入到数据块当中,最后还需要建立数据块和inode
结构的对应关系。
- 删除文件做了些什么?
- 将该文件对应的
inode
在Inode map
当中设置为无效。- 将该文件申请过的
Data Block
在Block map
当中置为无效。
这也是我们为什么删除一个软件的速度比下载同一个软件的速度快的多的原因。当然因为文件内容并没有被删除,所以我们可以在对应内容被其他文件内容覆盖之前,通过一些技术手段复原已删除文件。
- 如何理解目录?
目录也是一种文件,是文件就有对应的文件属性与文件内容,其中对应的文件属性就是我们的
inode
存储的就是目录的大小,目录的拥有者等。而对应的文件内容存储的就是该目录下的文件名以及对应文件的inode
指针。
4. 软硬链接
4.1 软链接
软链接又叫做符号链接,软链接文件是一个独立的文件,该文件有自己的inode
号,但是该文件只包含了源文件的路径名,所以软链接文件一般就是对应路径文件的一种快捷访问方式。其中在Windows
系统中,我们桌面上软件图标就是访问对应程序的快捷方式,本质其实就是一个软连接文件。
在Linux
中,我们可以通过指令ln -s 文件名 软链接名
设置软连接:
并且我们也能通过指令unlink 软连接名
取消对应的软连接,并且如果一旦删除软连接所指向的文件,那么该软连接文件也将毫无意义。
4.2 硬链接
硬链接文件就是源文件的一个别名,它与源文件之间具有相同的inode
,大小。一旦为某个文件建立硬链接,那么对应的硬链接数就会加一。
硬链接没有独立的inode
,并不是一个独立的文件, 本质是在特定的目录下,添加一个文件名和inode
编号的映射关系。
在Linux
中,我们可以通过指令ln 文件名 硬链接名
建立对应的硬链接,同样我们也能通过unlink 硬链接名
取消对应的硬链接。
但是我们还可以提出一个疑惑就是,我们的普通文件的硬链接数为1,但是目录的硬链接数为什么不为1呢?
因为我们当前目录下还存在一个隐藏文件.
指向我们的当前目录,这个.
文件其实就是我们的目录的硬件链接文件。
5. 文件时间-ACM
在Linux
中,我们可以使用指令stat 文件名
来查看对应文件的信息。
以下是对应关于文件时间的信息。
Access
: 文件最后被访问的时间。Modify
: 文件内容最后的修改时间。Change
: 文件属性最后的修改时间。
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify
的改变会影响Change
一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change
的改变不会影响Modify
的改变。
我们若是想将文件的这三个时间都更新到最新状态,可以使用指令touch 文件名
。
6. Linux下一切皆文件
最后我们学习完文件相关的知识后,再来谈一谈linux
下一切皆文件这一观点。
在Linux
系统中,“一切皆文件”是一个重要的设计理念。这一理念的实现涉及到多个层面的技术和机制。
首先,外设与内存进行交互,像键盘、显示器等外设都有诸如read
、write
等读写方法。但由于各种外设的硬件结构不同,这些方法在底层实现是不一样的,并且都是在硬件的驱动层完成。
那么,Linux
是如何做到“一切皆文件”的呢?首先Linux
引入了软件的虚拟层 VFS
(虚拟文件系统)。VFS
会统一维护每一个文件的结构体struct file
,这个结构体包含了一批函数指针。这些函数指针能够直接指向底层的方法。在上层,我们可以以统一的struct_file
的方式去看待文件。因此,“一切皆文件”指的是在 VFS
层面上的看待方式,而非在驱动层。
这种实现方式与C++中的多态类似。在 C++中,父类指针指向谁,调用的就是谁的方法。在 C 语言中,可以通过函数指针做到指向不同的对象时执行不同的方法,实现多态的性质。在Linux
中,每个struct file
中包含很多函数指针,这样在struct file
上层看来,所有的文件都是调用统一的接口,而在底层则通过函数指针指向不同硬件的方法,实现与具体硬件对应的逻辑。