[vale os_3] 文件系统/VFS | 网络协议栈
第四章:文件系统 / VFS
欢迎回来!
- 在第一章:Kconfig中,我们学习了如何为openvela
系统选择组件
。 - 在第二章:构建系统中,我们看到了这些选择
如何被构建成可运行的软件
。 - 在第三章:驱动程序中,我们探索了称为驱动程序的特定
软件模块如何让操作系统与串口、LED或传感器等独立硬件组件通信
,通常使它们以特殊"设备节点"的形式出现在/dev
目录中。
现在,我们将深入探讨管理这些设备节点和处理存储设备上传统文件的系统:文件系统,特别是通过 虚拟文件系统(VFS)层。
什么是文件系统 / VFS?统一管理器
想象你的嵌入式系统有多个可能找到"数据"的地方:
- 某些数据来自硬件设备,如报告温度的传感器。
- 某些数据需要发送到设备,如控制电机的命令。
- 其他数据可能永久存储在闪存芯片或SD卡上,如配置文件、日志或保存的应用程序数据。
传统上,与传感器交互、控制电机和从SD卡读取文件都需要完全不同的函数集和编程方法。这将使应用程序开发变得非常复杂。
由虚拟文件系统(VFS)层协调的文件系统通过提供统一方式来访问不同类型的数据源和目的地解决了这个问题。
将VFS视为通用接待员或图书馆目录与管理系统:
- 它提供单一的标准化接口(
open
、read
、write
、close
等),应用程序无论与硬件设备通信还是从存储设备读取文件都可以使用。 - 就像图书馆目录告诉你在哪里找到书籍(主书库、特藏等),VFS知道将请求路由到哪里:是给驱动程序管理的设备,还是给特定存储文件系统管理的文件?
- 它将所有可访问的内容组织成单一的层次树结构(如计算机上的文件夹和文件)。
这种设备和文件使用相同函数集进行访问的方法,通常被称为类Unix操作系统中的**“万物皆文件”**哲学。
核心理念:通过路径访问万物
核心概念是openvela中大多数交互资源(硬件设备、数据文件)都可以通过文件系统类层次结构中的路径访问,从根目录(/
)开始:
- 硬件设备: 如第三章:驱动程序所示,驱动程序管理的设备通常显示为称为设备节点的特殊条目,通常位于
/dev
目录下(例如/dev/uart0
、/dev/led1
、/dev/temp_sensor
)。 - 真实文件: 存储在物理存储(如Flash或SD卡)上的文件出现在该存储文件系统"挂载"的路径下(例如
/var/log.txt
、/mnt/sdcard/photo.jpg
)。 - 特殊系统信息: 类似Linux的
/proc
,openvela可能通过"伪"文件系统暴露系统信息(不过在基本配置中不如/dev
突出)。
应用程序在调用open("/dev/uart0", ...)
或open("/var/config.txt", ...)
时不需要知道区别。VFS处理将请求路由到正确的底层处理器。
虚拟文件系统(VFS):路由器和标准化器
**虚拟文件系统(VFS)**是实现这一可能的软件层。它位于应用程序使用的标准系统调用(如open()
、read()
、write()
、close()
)和实际处理请求的代码(驱动程序或特定存储文件系统)之间。
当应用程序在文件描述符上调用read()
时:
- VFS接收调用。
- 查找文件描述符以确定对应的底层资源。
- 如果是设备节点(如
/dev/uart0
),VFS调用UART驱动程序实现的read
函数。 - 如果是普通文件(如
/var/log.txt
),VFS调用FAT文件系统驱动实现的read
函数(假设/var
挂载为FAT分区)。
VFS隐藏了细节,提供一致的编程接口。
VFS和驱动程序管理的设备节点
我们在第三章:驱动程序中提到了设备节点。让我们回顾VFS如何与它们交互。
当驱动程序初始化时(通常在系统启动期间),它调用register_driver()
在VFS层次结构中创建设备节点,通常在/dev
下。
// 简化的驱动程序注册示例
#include <nuttx/fs/fs.h> // 包含register_driver// 假设led_fops是LED驱动的struct file_operations
extern const struct file_operations led_fops;int led_driver_init(...) {// ... 驱动程序硬件设置 ...// 在/dev/led0注册驱动程序int ret = register_driver("/dev/led0", &led_fops, 0666, NULL);if (ret < 0) {// 处理错误return ret;}return 0;
}
这个调用告诉VFS:“嗨,在/dev/led0
有一个设备,当有人尝试对该路径进行open
、read
、write
等操作时,你应该调用这些函数(&led_fops
)”。
当应用程序稍后调用open("/dev/led0", O_WRONLY)
时,VFS:
- 在树结构中查找路径
/dev/led0
。 - 找到由
register_driver
创建的条目。 - 看到该条目与
led_fops
结构关联。 - 分配一个
struct file
来表示这个打开的实例。 - 调用
led_fops
结构中的open
函数指针,传递struct file
。 - 向应用程序返回文件描述符(
fd
),本质上是应用程序打开struct file
实例列表的索引。
后续的write(fd, ...)
等调用将类似地由VFS路由到与该fd
关联的struct file
中存储的led_fops
结构中的write
函数指针。
真实文件系统:管理存储
除了设备节点,VFS还管理驻留在物理存储设备(如闪存(NAND/NOR)或可移动介质(SD卡))上的真实文件系统。
这些存储设备通常通过块驱动程序访问。块驱动程序提供在设备上以固定大小"块"或"扇区"读写数据的标准方式,隐藏底层硬件细节。VFS使用struct block_operations
接口(在file_system.md
片段中简要提到)与块驱动程序交互。
存储文件系统(如FAT、ROMFS等)是知道如何在块设备提供的块内组织文件和目录的软件。它管理:
- 目录结构(文件夹)
- 文件名和权限
- 跟踪哪些块属于哪些文件
- 空闲空间管理
openvela支持的文件系统示例(通过Kconfig启用):
- ROMFS: 简单的只读文件系统,用于构建时存储在ROM/Flash中的数据。
- FAT(FAT12、FAT16、FAT32): 广泛兼容的SD卡和U盘文件系统。
- LittleFS: 专为小型SPI NOR闪存设计的日志文件系统。
- EXFAT: 常用于较大SD卡和U盘的文件系统。
- TMPFS: 将一切存储在RAM中的文件系统(临时存储)。
- NXFFS: 较旧的NuttX专用NOR闪存磨损均衡文件系统。
挂载文件系统:将存储附加到树
要访问存储设备上的文件,必须将存储文件系统挂载到VFS树的特定目录路径(称为挂载点)。
使用mount()
系统调用(如file_system.md
片段所示)完成此操作:
#include <sys/mount.h> // 包含mount()// 示例:将块设备/dev/mmcsd0上的FAT文件系统挂载到VFS树的/mnt/sdcard路径
int mount_sdcard() {const char *source = "/dev/mmcsd0"; // 块设备路径const char *target = "/mnt/sdcard"; // VFS树中的路径(挂载点)const char *filesystemtype = "vfat"; // 文件系统类型(FAT)unsigned long mountflags = 0; // 标准标志const void *data = NULL; // 可选的特定文件系统数据// 确保目标目录存在// mkdir("/mnt/sdcard", 0777); // 通常需要先创建int ret = mount(source, target, filesystemtype, mountflags, data);if (ret < 0) {perror("挂载失败"); // 打印错误信息return -1;}printf("成功将%s挂载到%s\n", source, target);return 0;
}
mount()
期间发生了什么?
参考file_system.md
片段中的mount()
描述和关键代码:
- 调用
mount()
函数,参数包括块设备路径(source
)、VFS树中的目标路径(target
)和filesystemtype
。 - VFS查找
filesystemtype
(“vfat”)以找到FAT文件系统驱动对应的struct mountpt_operations
(使用片段中的g_bdfsmap
映射)。 - 查找
source
路径(/dev/mmcsd0
)以找到表示块设备驱动的inode
。 - 查找或创建
target
路径(/mnt/sdcard
)的inode
。如果不存在,则调用inode_reserve
。如果存在,则检查是否为有效目录/挂载点。 - 关键步骤:调用FAT文件系统的
struct mountpt_operations
中的bind
函数。bind
函数初始化文件系统软件,从块设备(/dev/mmcsd0
)读取其结构,并准备进行文件操作。它可能存储文件系统特定数据(如缓存的目录信息)并返回handle
。 - VFS更新
/mnt/sdcard
的inode
,将其标记为挂载点(INODE_SET_MOUNTPT
),并在i_private
字段中存储FAT文件系统的struct mountpt_operations
和bind
返回的handle
。
成功挂载后,任何以/mnt/sdcard
开头的VFS操作(如open
、readdir
)将被路由到已挂载FAT文件系统驱动的struct mountpt_operations
函数。
伪文件系统:基础
即使没有物理存储挂载,openvela也有基本的基于内存的伪文件系统。通过CONFIG_NFILE_DESCRIPTORS > 0
启用:
- 提供根目录(
/
)。 - 通常是驱动程序注册设备节点(
/dev/...
)的位置。 - 包含
/dev
、/var
、/tmp
、/mnt
等目录的inode
,这些目录作为真实文件系统的潜在挂载点。
这个伪文件系统为VFS树提供了基本结构,即使没有持久存储。
关键数据结构(简化版)
理解VFS涉及几个核心数据结构(在file_system.md
片段中引用):
-
struct inode
:表示VFS树中的任何内容——目录、普通文件、字符设备、块设备、挂载点等。层次结构中的每个条目都有一个inode
。i_peer
、i_child
:构建目录树的链接。i_crefs
:引用计数(该节点被使用的次数)。i_flags
:指示类型(目录、驱动、挂载点等)。u
:包含指向适合i_flags
的操作(file_operations
、block_operations
、mountpt_operations
)的联合体。i_private
:私有数据指针,通常指向驱动程序的内部状态或bind
后的文件系统句柄。i_name
:在父目录中的条目名称。
-
union inode_ops_u
:struct inode
中的联合体,保存可操作该节点类型的函数指针。VFS使用i_flags
确定联合体中哪个指针有效以及使用哪组操作。i_ops
:指向字符驱动的struct file_operations
。i_bops
:指向块驱动的struct block_operations
。i_mops
:指向挂载点的struct mountpt_operations
(处理文件系统操作)。
-
struct file_operations
:字符设备和某些伪文件的标准接口。包含open
、close
、read
、write
、ioctl
等函数指针(参见第三章:驱动程序和片段)。 -
struct mountpt_operations
:已挂载文件系统的标准接口。包含类似file_operations
的函数(针对挂载FS内打开文件的open
、close
、read
、write
、ioctl
),加上文件系统特定的函数如bind
、unbind
、opendir
、readdir
、stat
、mkdir
、unlink
等(参见片段)。 -
struct file
:表示特定任务打开的文件或设备节点实例。当应用程序调用open()
时,VFS分配struct file
。f_oflags
:打开时使用的标志(只读、只写等)。f_pos
:文件/设备中的当前读写位置。f_inode
:指回该struct file
实例访问的struct inode
。f_priv
:此特定打开实例的私有数据(与inode
的i_private
不同)。
每个任务都有struct file
指针列表(struct filelist
),open()
返回的文件描述符(fd
)本质上是该列表的索引。
流程:从应用到实现
让我们追踪read()
调用的工作流程,取决于它是针对设备还是存储上的文件。
该图展示了VFS(使用inode
结构和关联的操作集)如何作为中央调度器,根据路径将请求路由到驱动或特定文件系统实现。
Kconfig与文件系统
与驱动程序类似,文件系统支持使用Kconfig配置(第一章:Kconfig):
CONFIG_NFILE_DESCRIPTORS
:启用基本伪文件系统和文件描述符支持。CONFIG_FS_ROMFS
、CONFIG_FS_FAT
、CONFIG_FS_LITTLEFS
等:包含特定存储文件系统实现的选项。CONFIG_FS_NXFFS
、CONFIG_FS_SMARTFS
:旧版NuttX文件系统的选项。CONFIG_FS_PROCFS
、CONFIG_FS_BINFS
:其他类型伪文件系统的选项。- 与特定文件系统特性相关的选项(例如FAT的长文件名支持、闪存文件系统的磨损均衡)。
- 启用存储硬件块驱动的选项(例如
CONFIG_MMCSD
)。
使用menuconfig
启用项目所需的文件系统。
结论
-
由虚拟文件系统(
VFS
)协调的文件系统是openvela中的关键层,它提供了统一灵活的方式来与硬件设备和物理存储介质上的数据进行交互。 -
通过使用路径表示资源和标准操作集(
open
、read
、write
),VFS简化了应用程序开发,使系统更具模块化。 -
理解VFS、设备节点、真实文件系统和挂载过程是管理数据和与嵌入式系统中多样化硬件交互的关键。
在下一章,我们将探索网络协议栈,这是另一个常与VFS集成的复杂系统,用于提供网络接口访问,有时甚至以设备类条目形式出现或需要文件类套接字操作。
第五章:网络协议栈
在前面的章节中,我们已经建立了对openvela系统的基础理解。
- 在第一章:Kconfig中,我们学习了
如何选择需要的组件
; - 在第二章:构建系统中,了解了这些
组件如何被组装成运行程序
; - 在第三章:驱动程序中,探索了
程序如何与独立硬件组件通信
; - 在第四章:文件系统/VFS中,掌握了系统如何以
统一方式管理硬件访问
(通过/dev
等设备节点)和存储数据。
现在,让我们思考如何将嵌入式系统连接到外部世界,特别是通过互联网或本地网络与其他设备通信。
我们的设备如何向服务器发送数据、从手机接收指令,甚至从网站获取信息?这正是网络协议栈的职责所在。
何为网络协议栈?与世界对话
想象您要寄送包裹给远方的人。流程并非简单将物品投入邮筒:
- 将物品装入盒子
- 包裹填充物并密封
- 在外包装清晰标注收件/发件地址
- 交付邮局或物流公司
- 物流系统添加标签条码,规划运输路线(陆运/空运/海运)
- 沿途各节点根据地址信息路由包裹
- 最终收件人逐层拆封获取物品
网络通信遵循相似原理!数据需经过精心封装、寻址和路由,才能跨越有线/无线介质,被目标设备和特定应用正确接收解析。
网络协议栈是管理网络数据传输的规则、流程和软件层集合,定义了数据的:
- 封装与解封装
- 寻址(网络地址分配)
- 路由(正确路径选择)
- 可靠性传输(TCP)或快速传输(UDP)
- 跨应用解析
它将复杂网络通信任务分解为不同层级处理。
分层架构的优势
分层网络通信架构具备多重优势:
- 复杂度管控:每层专注特定功能(如物理连接、跨网络路由、可靠性保障)
- 模块化设计:更换某层实现(如以太网转WiFi)无需修改上层
- 标准化协议:各层协议(IP/TCP/UDP)行业通用,实现跨厂商设备互通
- 抽象隔离:应用无需了解底层传输细节,只需与上层协议交互
协议栈层级
尽管存在OSI七层模型等不同模型,openvela主要采用TCP/IP四层模型:
TCP/IP层级 | OSI对应层 | 核心功能 | 物流类比 |
---|---|---|---|
应用层 | 应用/表示/会话层 | 提供应用服务(HTTP网页/FTP文件传输等) | 实际寄送物品(礼物/文件) |
传输层 | 传输层 | 管理跨设备应用通信,添加端口号(类似门牌号),处理可靠性 | 选择物流服务(挂号信/明信片),附加服务选项 |
网络层 | 网络层 | 跨网络设备通信,添加IP地址(街道/城市/国家) | 填写收/发件地址,规划全局路线 |
数据链路层 | 数据链路层 | 直连设备通信,添加MAC地址(建筑编号),准备物理介质 | 本地派送指令,建筑编号标注 |
物理层 | 物理层 | 物理硬件实现(线缆/连接器/无线电波) | 道路/卡车/飞机等运输实体 |
openvela网络栈主要实现传输层、网络层和数据链路层。
应用层接口:套接字(Socket)
openvela应用通过套接字接口与协议栈交互,这是标准POSIX套接字接口,包含以下核心函数:
#include <sys/socket.h>// 创建IPv4 TCP套接字示例
int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字if (sockfd < 0) {perror("socket创建失败");return 1;}struct sockaddr_in serv_addr = {.sin_family = AF_INET,.sin_port = htons(8080), // 目标端口8080.sin_addr.s_addr = inet_addr("192.168.1.1") // 目标IP};if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {perror("连接失败");close(sockfd);return 1;}char buffer[1024];ssize_t bytes_recv = recv(sockfd, buffer, sizeof(buffer), 0); // 接收数据close(sockfd);return 0;
}
套接字描述符(sockfd
)类似于文件描述符,通过VFS路由系统调用,但网络通信有独立处理逻辑。
数据传输流程
发送数据流程:
接收数据流程:
网络驱动注册
网络接口通过netdev_ops_s
结构注册到协议栈:
struct netdev_ops_s {/* 接口控制 */int (*ifup)(FAR struct netdev_lowerhalf_s *dev);int (*ifdown)(FAR struct netdev_lowerhalf_s *dev);/* 数据收发 */int (*transmit)(FAR struct netdev_lowerhalf_s *dev, FAR netpkt_t *pkt);int (*receive)(FAR struct netdev_lowerhalf_s *dev, FAR netpkt_t *pkt);/* 状态获取 */int (*get_macaddr)(FAR struct netdev_lowerhalf_s *dev, FAR uint8_t *mac);
};// 驱动注册示例
int ethernet_driver_register() {struct netdev_lowerhalf_s *dev = kmm_zalloc(sizeof(...));dev->ops = ðernet_ops; // 绑定操作集return netdev_lower_register(dev, NET_LL_ETHERNET);
}
Kconfig网络配置
通过Kconfig启用网络功能:
CONFIG_NET=y # 启用网络协议栈
CONFIG_NET_TCP=y # 启用TCP协议
CONFIG_NET_IPv4=y # 启用IPv4
CONFIG_NETDEVICES=y # 启用网络设备框架
CONFIG_ETH0_DRIVER=y # 启用以太网驱动
CONFIG_NETUTILS_PING=y # 包含ping工具
高级功能
openvela支持以下进阶特性:
- NAT网络地址转换:实现
IP共享
- DHCP客户端/服务端:
自动IP分配
- 零拷贝传输:提升吞吐性能
- 多协议支持:
TCP/UDP/ICMP/IPv6
- 虚拟网络设备:TUN/TAP接口
- 流量整形:QoS服务质量控制
网络测试工具
启用以下工具进行诊断:
ping 192.168.1.1 # ICMP连通性测试
iperf -c 192.168.1.1 # 带宽测试
tcpdump -i eth0 # 抓包分析
总结
网络协议栈是嵌入式系统连接能力的核心。
通过分层模型和标准化接口,实现了从物理信号到应用数据的完整解析。
结合Kconfig配置和驱动开发,可使openvela设备灵活适应各类网络环境。
下一章将深入探讨处理器间通信(IPC),解析多核/分布式系统中的协同工作机制
~