个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创Linux系统基础-文件系统
收录于专栏[Linux学习]
本专栏旨在分享学习Linux的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1. 回顾C语言文件接口
hello.c 写文件
hello.c 读文件
输出信息到显示器
stdin & stdout & stderr
总结
2. 系统文件IO
hello.c写文件
hello.c读文件
系统接口介绍
open
需要的头文件:
方法:
参数:
返回值
系统调用和库函数
3. 文件描述符fd
0 & 1 & 2
文件系统结构初步了解
文件描述符的分配规则
重定向
1. > (输出重定向)
2. >> (追加重定向)
3. < (输入重定向)
4. FILE结构体
5. 理解文件系统
编辑
Linux ext2文件系统
硬链接
硬链接的应用:
软连接
软连接的应用:
1. 回顾C语言文件接口
hello.c 写文件
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp) printf("fopen error!\n");
const char* msg = "hello C programming language";
int count = 5;
while(count--) fwrite(msg, strlen(msg), 1, fp);
fclose(fp);
return 0;
}
FILE *fp:定义一个指向 FILE 结构的指针 fp,用于处理文件操作。
fopen("myfile", "w"):
尝试以写入模式("w")打开名为 "myfile" 的文件。
如果文件不存在,fopen 会创建一个新的空文件。
如果文件已经存在,内容会被清空。
fwrite(msg, strlen(msg), 1, fp):
msg:要写入的内容。
strlen(msg):要写入的字节数(字符串的长度)。
1:要写入的块数。
fp:目标文件指针。
使用 fclose(fp) 关闭打开的文件,确保所有缓存的内容被写入磁盘,并释放系统资源。
运行结果:
注意 :
在C/C++编程中, 如果打开一个文件后不调用fclose()来关闭它, 可能会导致一下集中后果:
1. 资源泄露'
没打开一个文件, 操作系统都会分配一定的资源(如文件句柄或文件描述符). 如果不关闭文件, 系统的资源将持续占用, 最终可能导致资源耗尽, 无法打开新的文件或进行其他操作, 这种情况在长时间运行的程序或需要频繁打开文件的程序中特别明显(一旦发现, 后果不堪设想~)
2. 数据末写入
文件通常会使用缓冲区来提高写入效率, 在调用fwrite(), fprintf() 等函数时, 数据首先被写入到内存缓冲区中, 而不是立即写入文件, 如果程序在未调用fclose()的情况下终止(例如, 异常, 崩溃或使用exit()), 缓冲区中的数据可能不会被写入到文件中, 从而导致数据丢失~
hello.c 读文件
int main()
{
FILE* fp = fopen("myfile", "r");
if(!fp) printf("fopen error!\n");
char buf[1024];
const char * msg = "hello linux!\n";
while(1)
{
size_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)) break;
}
fclose(fp);
return 0;
}
使用 fopen 函数以只读模式("r")打开名为 "myfile" 的文件。如果文件打开失败(如文件不存在),fopen 将返回 NULL,程序会打印错误信息。
while(1) 创建一个无限循环。
使用 fread 函数从文件中读取数据:
第一个参数是目标缓冲区 buf,第二个参数是每次读取的字节大小(1),第三个参数是要读取的字节数(这里是 strlen(msg),即 13 字节)。
如果 fread 成功读取到数据,则 s 将大于 0。
通过 buf[s] = 0; 将读取的数据结尾标志设置为 '\0',以确保后续的 printf 函数正确打印字符串。
然后使用 printf 输出读取到的内容。
使用 feof(fp) 检查文件是否结束,如果是,则跳出循环。
最后关闭打开的文件,释放系统资源。
结果展示:
可以看到, 已经将我们之前写入文件的信息全部打印出来了~
输出信息到显示器
#include <stdio.h>
#include <string.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;
}
fwrite 函数用于从内存块写入数据到文件或输出流。
第一个参数是待写入的内存地址,这里是 msg。
第二个参数是每个元素的大小,这里是 strlen(msg),表示字符串的长度(即 14 字节,包括换行符)。
第三个参数是要写入的元素个数,这里为 1,表示写入一个块。
第四个参数是目标输出流,这里是 stdout,标准输出。
printf 是一个格式化输出函数,输出字符串 "hello printf\n" 到标准输出。
fprintf 是一个格式化输出函数,功能与 printf 类似,但可以将输出写入指定的文件流。
第一个参数是目标输出流,这里为 stdout,第二个参数是待输出的字符串。
这行代码的效果与 printf 相同,即将字符串 "hello fprintf\n" 输出到标准输出。
结果展示:
stdin & stdout & stderr
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
我们可以使用man手册直接在linux中查看:
总结
打开文件的方式:
r Open text file for reading. The stream is positioned at the beginning of the file.
r+ Open for reading and writing. The stream is positioned at the beginning of the file.
w Truncate file to zero length or create text file for writing. The stream is positioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file). The file is created if it does not exist. Output is always appended to the end of the file. POSIX
is silent on what the initial read position is when using this mode. For glibc, the initial file position for reading is at the beginning of the file, but
for Android/BSD/MacOS, the initial file position for reading is at the end of the file.
翻译后为 :
1. r : 打开文件进行阅读, 流位于文件的开头.
2. r+ : 打开读写模拟. 流处于文件的开头位置
3, w : 文件截断为零长度或创建用于写入的文本文件, 流被置于文件的开头
4. w+ : 打开文件以供读写, 如果文件不存在, 则创建它; 否则将其截断. 流的位置将设置为文件的开头.
5. a : 文件处于可追加状态 (可以在文件末尾写入数据). 如果文件不存在, 则创建该文件, 流处于文件末尾的位置.
6. a+ : 用于读取和追加文件 (在文件末尾写入数据), 如果文件不存在, 则会创建文件, 输出始终被追加到文件的末尾
2. 系统文件IO
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码
hello.c写文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello world!\n";
int len = strlen(msg);
while(count--)
{
write(fd, msg, len);
}
close(fd);
return 0;
}
umask(0):设置进程的文件模式创建掩码为 0。这意味着后续创建的文件将使用其指定的权限,不受默认掩码的影响。通常情况下,系统会有一个默认的掩码,设置为 0 可以确保文件创建时使用的权限为0644。(可以看到最后我们创建的文件是rw r r, 也就是我们要求的0644)
open 函数用于打开文件。
第一个参数 "myfile" 是要打开或创建的文件名。
第二个参数是打开模式,这里使用了两个标志:
O_WRONLY:以只写模式打开文件。
O_CREAT:如果文件不存在,则创建一个新文件。
这里我使用 | 或起来, 也就以只写模式打开文件, 文件如果不存在就创建一新个文件.
第三个参数 0644 是文件权限,仅在文件创建时有效。它表示:
所有者具有读写权限(6),
同组用户具有读权限(4),
其他用户也具有读权限(4)。
fd 是返回的文件描述符。如果打开失败,返回值将为负数。
write(fd, msg, len);:
第一个参数 fd 是文件描述符。
第二个参数 msg 是要写入的内容。
第三个参数 len 是要写入的字节数。
结果展示:
hello.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("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
const char* msg = "hello world!\n";
char buf[1024];
while(1)
{
size_t s = read(fd, buf, strlen(msg));
if(s > 0) printf("%s", buf);
else break;
}
close(fd);
return 0;
}
open 函数用于打开文件。
第一个参数 "myfile" 是要打开的文件名。
第二个参数 O_RDONLY 表示以只读模式打开文件。
fd 是返回的文件描述符。如果打开失败,返回值将为负数。
size_t s = read(fd, buf, strlen(msg));:
使用 read 函数从文件描述符 fd 读取数据。
第一个参数 fd 是文件描述符。
第二个参数 buf 是用来存储读取数据的缓冲区。
第三个参数是 strlen(msg),即读取 msg 字符串的长度(12 字节)。
if(s > 0):
检查返回的字节数 s 是否大于 0。如果大于 0,表示成功读取了数据。
printf("%s", buf);:将缓冲区 buf 中的数据打印到标准输出。
结果展示:
系统接口介绍
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 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件 的默认权限,否则,使用两个参数的open。
write, read, close, 了seek, 类比c文件相关接口~
系统调用和库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
如下图所示:
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
3. 文件描述符fd
通过对open函数的学习,我们知道了文件描述符就是一个小整数
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
char buf[1024];
size_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
read(0, buf, sizeof(buf));:使用 read 函数从标准输入(文件描述符 0)读取数据。
第一个参数 0 表示从标准输入读取。
第二个参数 buf 是用于存储读取数据的缓冲区。
第三个参数 sizeof(buf) 表示读取的最大字节数(即 1024 字节)。
write(1, buf, strlen(buf));:
使用 write 函数将 buf 中的数据写入标准输出(文件描述符 1)。
strlen(buf) 计算 buf 中的字符串长度,以确保写入的字节数正确。
write(2, buf, strlen(buf));:
同样使用 write 函数将 buf 中的数据写入标准错误(文件描述符 2)。
注意:标准输出和标准错误是两个不同的输出流,通常在终端中显示在同一位置,但在文件重定向时,可以分开处理。
结果展示:
文件系统结构初步了解
根据以上我们的分析, 可以得到文件系统的初步结构, 如下图:
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件!~
文件描述符的分配规则
代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是 fd: 3
关闭0或者2,在看:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(0);
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符.
重定向
那如果关闭1呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
例如:
1. > (输出重定向)
> 符号用于将命令的标准输出重定向文件, 如果文件已经存在, 则会被覆盖 (如果目标文件已经存在, 则其内容会被覆盖~~)
2. >> (追加重定向)
>> 符号用于将命令的标准输出追加到文件的末尾, 如果目标文件不存在, 则会创建该文件
3. < (输入重定向)
< 符号用于将文件的内容作为命令的标准输入, 它可以将文件的内容传递给命令, 而不是从键盘输入~
4. FILE结构体
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE结构体内部,必定封装了fd
来段代码在研究一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.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(msg1), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和 fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供
5. 理解文件系统
经过上面的铺垫, 现在我们正式认识一下LinuxOS下的文件系统:
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据
每行包含7列:
1. 模式 2. 硬链接数 3. 文件所有者 4, 组 5, 大小 6. 最后修改时间 7. 文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
我们可以看到text.c文件上面有Inode, 为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统
如下图所示:
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设 定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
对上面名词的解释:
1. Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
2. 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
3. GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
4. 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
5. inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
6. i节点表:存放文件属性 如文件大小,所有者,最近修改时间等
7. 数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
创建一个新文件主要有一下4个操作:
1. 存储属性 内核先找到一个空闲的i节点(这里是528160)。内核把文件信息记录到其中。 2. 存储数据 该文件需要存储在三个磁盘块,内核找到了三个空闲块:300, 500, 800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
3. 记录分配情况 文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(528160,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
文件名和inode编号具有映射关系!!
1. 一个目录下不能建立同名文件
2. 查找文件的顺序 : 文件名 -> inode编号
总结 :
文件 = 内容 + 属性(也是数据)
文件在磁盘存储 本质是: 文件的内容 + 文件的属性数据
Linux文件系统特定: 文件内容 和 文件属性 分开存储
找到指定的文件 -> 文件所在目录(目录也是文件) -> 根据文件名 ->(映射) inode -> 目标文件
硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个 inode。
abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 528160的硬连接数为2。
我们在删除文件时干了两件事情:
1.在目录中将对应的记录删除,
2.将硬连接数-1,如果为0,则将对应的磁盘释放。
总结 : 硬链接不是一个独立的文件, 因为你没有独立的inode number, 你用的是目标文件的inode
硬链接就是一个文件名和inode的映射关系, 建立硬链接, 就是在指定目录下, 添加一个新的文件名和inode number的映射关系!
硬链接的应用:
任何一个目录, 刚开始新建的时候, 引用计数一定是2:
目录1内部, 新建一个目录, 会让1目录的引用计数自动+1, 一个目录内部有几个目录 : 1引用计数-2
所以我们可以使用cd .. cd .进行路径访问
总结:
数据冗余:在不占用额外空间的情况下创建多个引用。
保护数据:如果删除了某个硬链接,文件内容仍然存在,只要还有其他硬链接指向该 inode。(一般常用硬链接做备份)
方便访问:可以使用不同的名字或路径访问同一个文件。
软连接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法为:
软链接是一种指向另一个文件路径的引用,类似于 Windows 系统中的快捷方式。它包含原始文件的路径信息,而不是直接指向文件数据。(所以大家别再认为删除桌面快捷方式就是把软件彻底删除了, 实际上你删除是它的软连接而已, 里面只有它的路径, 没有数据)
软连接的应用:
总结 :
简化路径:使用软链接可以简化复杂路径的访问。(快捷方式)
文件版本管理:可以为不同版本的文件创建软链接,方便切换。
跨文件系统的引用:可以在不同文件系统之间链接文件。