Linux基础 IO 和文件
目录
1. 问题引入---“流”
2. 站在系统角度理解文件操作
2.1 内核结构
2.2 基本系统接口和重定向
3. 站在编程语言的角度理解文件操作
4. 文件系统(ext2)
4.1 基本概念
4.2 组织管理方式
5. 软硬链接
6. 动静态库
1. 问题引入---“流”
程序运行 ——> 进程
通常,大部分现代的操作系统都会为每个进程默认打开 标准输入(键盘),标准输出(显示器)和标准错误(显示器) 流,因为这是进程与外部环境交互的基础!
在计算机科学中,“流”(Stream)是一种用于处理数据序列 的抽象概念。
它表示一个连续的数据传输过程,可以看作是数据在 程序之间 或 程序与硬件设备之间的动态传递。
特点:
1. 顺序性
2. 抽象化:将数据的来源和目的抽象为统一的接口,进行模块化设计,支持数据的分步操作,比如 网络流,管道流,...
3. 动态性:缓冲机制,优化数据的传输和处理效率
4. 双向性
怎么理解?—— Linux的核心设计哲学:一切皆文件
所以,本质是在处理:进程和文件的关系
//Linux Kernel 6.12.4
struct task_struct
{
//...
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
//...
}
2. 站在系统角度理解文件操作
2.1 内核结构
内核数据结构: struct file 的内容包括:
操作方法集:const struct file_operations *f_op;
struct file_operations {//函数指针
//......
ssize_t(*read) (...);
ssize_t(*write) (...);
//......
int (*mmap) (...);
int (*open) (...);
//......
} ;
文件状态:f_flags (打开模式和其它标志) ,f_mode (操作权限), ...
读写位置:f_pos
内存映射和关联的缓冲机制:struct address_space* f_mapping
引用计数和同步:
f_count (同一个文件可能被多个进程同时打开或使用,只有引用计数降为0时,内核才会释放管理此文件的资源)
truct rw_semaphore f_rwsem (同步控制)
......
文件指向的具体对象:
struct path f_path.dentry (指向文件对应的目录项结构,提供文件在目录树中的具体位置)
struct path f_path.mnt (指向挂载点信息,结合dentry 定位文件的绝对路径)
struct inode* f_inode (inode包含文件的元信息:大小,权限,所属,方法集,...)
. . . . . .
所以,开头说进程启动默认打开的3个标准流,本质就是 内核为其 “打开” 键盘文件和显示器文件 ——> 内核创建并管理一系列的内核数据结构!
接下来,我们不急于深入其中的原理,而是有了上面的认识后,如何通过让我们自己写的代码,编译运行后,做到:打开/编辑/关闭指定文件 ——> 也就是说,如何在操作系统内核已经提供了这样一套管理机制的基础之上,用系统原生接口,实现你自己的开发需求。
2.2 基本系统接口和重定向
1. 打开/创建文件
flags必须包含以下方式之一:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写)
此外,可以按需组合:O_ADDEND(追加) , O_CREAT(新建), O_TRUNC(清空),......
给如下一段演示代码:
#include <stdio.h>
#define Read 1 // 读 1
#define Write 2 // 号 10
#define Append 4 // 追加 100
#define Clear 8 // 清空 1000
void Test(int flags)
{
if (flags & Read)
{
printf("read\n");
if (flags & Write)
{
printf("write\n");
}
if (flags & Append)
{
printf("append\n");
}
if (flags & Clear)
{
printf("clear\n");
}
}
}
int main()
{
Test(Read);
printf("---------------------\n");
Test(Clear | Write);
printf("---------------------\n");
Test(Read | Write | Append);
return 0;
}
2. 读/写(单位:字节)
3. 关闭
4. 重定向
文件描述符的分配原则:从小往大,fd_arry数组的第一个可用NULL位置的下标!
所以,实现重定向的第一种方式就是:利用分配原则,“先关后开”。因为站在编程语言角度,比如:printf/scanf , cin/cout/cout 默认关联的就是fd: 0/1/2。再次重申:0/1/2只是下标!
另一种方式更简单易控制——系统接口:dup系列
3. 站在编程语言的角度理解文件操作
本质是在理解不同编程语言对 “文件流” 的抽象和封装实现。
以C语言为例:(glibc 2.17)
【请点击放大图片查看】
下面给一段测试代码验证 编程语言级别 缓冲区的存在和刷新:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
const char* str1 = "system call write str1 ";
write(1, str1, strlen(str1));
const char* str2 = "call lin func fwrite str2";
fwrite(str2, 1, strlen(str2), stdout);
sleep(5);//休眠5秒
const char* str3 = "call lin func fwrite str3\n";
fwrite(str3, 1, strlen(str3), stdout);
sleep(5);
return 0;
}
4. 文件系统(ext2)
4.1 基本概念
文件系统是操作系统中用于 组织,存储,管理和访问数据的一种方式。
它为存储设备(如HHD, SSD, U盘)上的数据提供了一种结构化的方式,使用户和应用程序能够方便的创建,修改,读取,删除文件和目录。
ext2 是一个简单高效的文件系统,适合基础文件存储需求。于1993年引入,最早在Linux 内核1.0(1994年发布)中作为默认文件系统被正式支持。
其最初是为机械磁盘(HDD)设计的,其数据结构和优化策略都充分考虑了机械磁盘的物理特性,包括旋转磁盘的寻道时间和数据访问的顺序性等。它的块组设计和inode机制为后续的文件系统(如ext3和ext4)奠定了基础,尽管现代系统更常用 ext4, 但学习ext2有助于理解Linux文件系统的设计理念。
磁盘的机械构成:
数据的写入和读取:
原理:磁性材料的磁化记忆和感应特性——> 两态 —— 0和1
4.2 组织管理方式
操作系统将 磁盘的存储抽象描述成 “连续的线性地址”,如下:
磁盘的基本IO单位是:扇区,512字节(典型值)
文件系统分配存储空间和进行 IO 操作的基本单位是: 块(block), 4KB (典型值)
接下来,就是 把这些逻辑地址块 和 系统数据管理相结合。简单来讲就是 数据存储需要遵守一定的规则,那么拿出来也是同一个逻辑。
如下图例:
Super Block(超级块):记录了文件系统的全局信息(struct ext2_super_block{...}), 它管理文件系统的状态,布局和操作参数,是文件系统挂载和操作的入口;在ext系列文件系统中,超级块只存在于一定数量的Block group中,用于容错和修复;通过超级块,文件系统能够高效地管理磁盘空间和文件资源。
挂载 :把已将写入我们这里说的这一套 “文件系统” 管理机制 或者其它文件系统 的分区关联到某目录下,此后在这个目录下的文件编辑数据都对应在此分区上。
即,挂载后,这个目录成为访问该分区的入口点!
本质就是:文件系统和内核路径管理(dentry, ...)间的关系!
而平常所说的 “格式化” 就是 把一套管理机制覆盖原有数据。
GDT(块组描述表):属性信息,用于管理
Block Bitmap(块位图):每个bit表示Data Block 中对应的数据块是否已被占用
inode Bitmao(inode位图):每个bit表示一个inode是否可用
所以:文件 = 属性(固定大小的 inode 结构体:128字节)+ 内容(Date Block)分开存储
看内核代码:(Kernel 1.0)
所以,在内核中,文件名其实不是文件的属性;一个文件的唯一性是根据 inode标识的!
//查看文件/目录的inode
bash: ls -i 或者 stat file_name
那么,你是否有这样的疑惑:既然文件名不是 文件的内核属性字段, 那么类似于 /usr/include/*** 这样以 ‘/’ 分隔,文件名构成的路径,不管在写代码,还是命令行参数,系统是如何做到正确解析识别的呢?
目录项 (Directory Entry)—— 充当了 文件名和对应的索引节点(Inode)之间的桥梁!
不同的文件系统可能有不同的目录项数据结构,但一般来说,目录项至少包含以下两个主要字段:
- 文件名:用于标识文件或目录的名称,是用户在文件系统中看到和使用的名称。
- Inode 编号:指向文件或目录对应的 Inode 的编号,通过这个编号可以在 Inode 表中找到对应的 Inode
其通常存储在目录文件中,每个目录文件实际上是一个包含多个目录项的列表 。
为了提高查找效率,Linux 内核使用了目录项缓存(dentry cache)和 索引节点缓存(Inode cache)。当系统访问一个文件或目录时,会先在目录项缓存/索引节点缓存中查找对应的,如果找到(缓存命中)则直接使用,避免了频繁的磁盘 I/O 操作。只有在缓存中找不到时,才会从磁盘上读取目录文件并更新缓存;或者操作系统也会根据当前的缓存使用,访问频率动态调整缓存,形成管理如下结构:
目录树(多叉树) 再说的简单点,路径解析,就是在这样的树形结构中做 匹配查找!
所以你知道,为什么同级目录下不能有同名文件,不同目录下可以有同名文件的原因了吧。
除此之外,struct inode中还有一个重要的部分,标识文件内容的存储位置:
现在看懂了吧!
5. 软硬链接
软链接是一个独立的文件,有自己的inode, 类似于快捷方式,保存源位置信息
硬链接没有新建文件,只增加新文件名和 inode的映射关系,即 目录项结构(dentry),引用计数+=1
注意:不允许用户给目录设置硬链接,否则会导致环路问题。
6. 动静态库
静态库(.a):程序在链接时把库的代码直接拷贝到可执行文件中,程序运行的时候将不再需要静态库。
动态库(.so): 程序在运行时链接动态库的代码,多个程序可同时共享库的代码。
库名 == 去掉前缀lib + 去掉后缀.a/.so ;比如:libc.so.6 ——> 库名就是 c
1. 如何打包形成静态库:Makefile
# 定义编译器和编译选项
CC = gcc
# 获取当前目录下所有的 .c 文件
SRCS = $(wildcard *.c)
# 根据 .c 文件生成对应的 .o 文件列表
OBJS = $(SRCS:.c=.o)
# 定义静态库的名称
LIB_NAME = libmy.a
# 默认目标,生成静态库
all: $(LIB_NAME)
# 生成静态库的规则
$(LIB_NAME): $(OBJS)
ar rcs $@ $^
# 编译 .c 文件生成 .o 文件的规则
%.o: %.c
$(CC) -c $< -o $@
# 清理规则,删除生成的 .o 文件和静态库
clean:
rm -f $(OBJS) $(LIB_NAME)
怎么用这个库:
把打包好的库 和 头文件拷贝到 编译器的可搜索路径下,就叫 安装。
或者指定 gcc /g++ 的 -I 选项,表示头文件的位置
如何通过形成的可执行文件反映链接方式:
1. 最简单的方式就是大小,静态链接因为拷贝接口的具体实现,一般比较大
2. ldd指令,只反映动态库的依赖关系,静态库不会在ldd 输出中体现
3. 反汇编和符号表检查
比如:objdump -d a.out | less
静态库的函数实现会直接出现在可执行文件中。
或者:
nm a.out | grep <function_name>
示例:
2. 动态库的打包和使用原理:Makefile
# 定义编译器和编译选项
CC = gcc
# 查找当前目录下所有的 .c 文件
SRCS = $(wildcard *.c)
# 将 .c 文件列表转换为对应的 .o 文件列表
OBJS = $(SRCS:.c=.o)
# 定义动态库的名称
SHARED_LIB = libmydynamic.so
# 默认目标,生成动态库
all: $(SHARED_LIB)
# 生成动态库的规则
$(SHARED_LIB): $(OBJS)
$(CC) -shared -o $@ $^
# 编译 .c 文件生成 .o 文件的规则; 生成位置无关码:-fPIC
%.o: %.c
$(CC) -fPIC -c $< -o $@
# 清理规则,删除生成的 .o 文件和动态库
.PHONY: clean
clean:
rm -f $(OBJS) $(SHARED_LIB)
使用方法和静态库的使用方法一致。
需要注意的是:
编译链接时的搜索路径 和 运行时的搜索路径是独立的,通常都是系统配置文件的默人路径,比如:/usr/include/ /usr/lin64/ 等!
所以,如果要保证程序能正常运行,通常有以下几种做法:
1. 拷贝 .so文件到系统共享路径下,通常值 /usr/lib/
2. 更改 LD_LIBRARY_PATH
3. ldconfig 配置 /etc/ld.so.conf.d/ ldconfig更新
动态库的原理:
和动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
如何做到的核心机制:PLT
· 在程序启动时,外部库(如libc)中的 函数地址尚未确定,PLT提供了一个跳转表,程序通过PLT调用外部函数,而不是直接跳转函数地址。
· 当第一次调用外部函数时,PLT会触发动态链接器(ld.so), 解析函数的真实地址,并将其存入GOT表中。
· 后续调用时,直接从GOT表中获取函数地址,避免重复解析,提高运行效率。
所以:PLT负责调用外部函数,起跳板作用
GOT保存函数和全局变量的实际地址
本篇分享到此结束,如果对你有所帮助,就是对小编最大的鼓励,可以的话,点赞,收藏并分享给你的小伙伴一起学习。
关注小编,持续更新中. . .