虚拟文件系统组件
概述
Virtual filesystem (VFS) component provides a unified interface for drivers which can perform operations on file-like objects. This can be a real filesystems (FAT, SPIFFS, etc.), or device drivers which exposes file-like interface.
虚拟文件系统(VFS)组件为可以对类文件对象执行操作的驱动程序提供统一接口。这可以是一个真正的文件系统(FAT,SPIFFS等),也可以是暴露文件类接口的设备驱动程序。
This component allows C library functions, such as fopen and fprintf, to work with FS drivers. At high level, each FS driver is associated with some path prefix. When one of C library functions needs to open a file, VFS component searches for the FS driver associated with the file’s path, and forwards the call to that driver. VFS also forwards read, write, and other calls for the given file to the same FS driver.
该组件允许C库函数(如fopen和fprintf)与FS驱动程序一起使用。在高级别,每个FS驱动程序都与某个路径前缀关联。当一个C库函数需要打开一个文件时,VFS组件搜索与该文件路径关联的FS驱动程序,并将该调用转发给该驱动程序。VFS还将给定文件的读取,写入和其他调用转发给同一FS驱动程序。
For example, one can register a FAT filesystem driver with /fat
prefix, and call fopen("/fat/file.txt", "w")
. VFS component will then call open
function of FAT driver and pass /file.txt
argument to it (and appropriate mode flags). All subsequent calls to C library functions for the returned FILE*
stream will also be forwarded to the FAT driver.
例如,可以使用/fat
前缀注册FAT文件系统驱动程序,然后调用fopen("/fat/file.txt", "w")
。之后VFS组件将调用FAT驱动程序的open
函数并将参数/file.txt
传递给它(以及适当的模式标志)。所有后续调用返回FILE*
流的C库函数也将被转发到FAT驱动程序。
FS注册
To register an FS driver, application needs to define in instance of esp_vfs_t
structure and populate it with function pointers to FS APIs:
要注册FS驱动程序,应用程序需要在esp_vfs_t
结构的实例中定义并使用指向FS API的函数指针填充它:
esp_vfs_t myfs = {
.flags = ESP_VFS_FLAG_DEFAULT,
.write = &myfs_write,
.open = &myfs_open,
.fstat = &myfs_fstat,
.close = &myfs_close,
.read = &myfs_read,
};
ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));
Depending on the way FS driver declares its APIs, either read
, write
, etc., or read_p
, write_p
, etc. should be used.
根据FS驱动声明的API,应该使用read
,write
…,或read_p
,write_p
…。
Case 1: API functions are declared without an extra context pointer (FS driver is a singleton):
情况1:API函数声明没有额外的上下文指针(FS驱动程序是单例):
ssize_t myfs_write(int fd, const void * data, size_t size);
// In definition of esp_vfs_t:
//在esp_vfs_t的定义中:
.flags = ESP_VFS_FLAG_DEFAULT,
.write = &myfs_write,
// ... other members initialized
// ...其他成员初始化
// When registering FS, context pointer (third argument) is NULL:
//注册FS时,上下文指针(第三个参数)为NULL:
ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));
Case 2: API functions are declared with an extra context pointer (FS driver supports multiple instances):
情况2:使用额外的上下文指针声明API函数(FS驱动程序支持多个实例):
ssize_t myfs_write(myfs_t* fs, int fd, const void * data, size_t size);
// In definition of esp_vfs_t:
//在esp_vfs_t的定义中:
.flags = ESP_VFS_FLAG_CONTEXT_PTR,
.write_p = &myfs_write,
// ... other members initialized
// ...其他成员初始化
// When registering FS, pass the FS context pointer into the third argument
// (hypothetical myfs_mount function is used for illustrative purposes)
// 注册FS时,将FS上下文指针传递给第三个参数
//(假设myfs_mount函数用于说明)
myfs_t* myfs_inst1 = myfs_mount(partition1->offset, partition1->size);
ESP_ERROR_CHECK(esp_vfs_register("/data1", &myfs, myfs_inst1));
// Can register another instance:
//可以注册另一个实例:
myfs_t* myfs_inst2 = myfs_mount(partition2->offset, partition2->size);
ESP_ERROR_CHECK(esp_vfs_register("/data2", &myfs, myfs_inst2));
路径
Each registered FS has a path prefix associated with it. This prefix may be considered a “mount point” of this partition.
每个注册的FS都有一个与其相关的路径前缀。这个前缀可能被认为是这个分区的“挂载点”。
In case when mount points are nested, the mount point with the longest matching path prefix is used when opening the file. For instance, suppose that the following filesystems are registered in VFS:
在嵌套挂载点的情况下,打开文件时将使用具有最长匹配路径前缀的挂载点。例如,假设以下文件系统在VFS中注册:
- FS 1 on /data
- FS 2 on /data/static
然后:
- FS 1 will be used when opening a file called
/data/log.txt
- FS 2 will be used when opening a file called
/data/static/index.html
Even if
/index.html"
doesn’t exist in FS 2, FS 1 will not be searched for/static/index.html
.当打开一个名为的文件时将使用FS 1
/data/log.txt
- 当打开一个叫做文件时,将使用FS 2
/data/static/index.html
- 即使
/index.html"
在FS 2中不存在,也不会搜索FS 1/static/index.html
。
As a general rule, mount point names must start with the path separator (/
) and must contain at least one character after path separator. However an empty mount point name is also supported, and may be used in cases when application needs to provide “fallback” filesystem, or override VFS functionality altogether. Such filesystem will be used if no prefix matches the path given.
通常,装载点名称必须以路径分隔符(/
)开头,并且必须在路径分隔符后至少包含一个字符。但是,也支持空的挂载点名称,并且可以在应用程序需要提供“fallback”文件系统或完全覆盖VFS功能的情况下使用。如果没有前缀匹配给定的路径,将使用这种文件系统。
VFS does not handle dots (.
) in path names in any special way. VFS does not treat ..
as a reference to the parent directory. I.e. in the above example, using a path /data/static/../log.txt
will not result in a call to FS 1 to open /log.txt
. Specific FS drivers (such as FATFS) may handle dots in file names differently.
VFS不对路径中的.
进行处理。VFS不将..
视为对父目录的引用。即在上面的例子中,使用路径/data/static/../log.txt
不会导致调用FS 1打开/log.txt
。特定的FS驱动程序(如FATFS)可能会以不同方式处理文件名中的点。(VFS不支持相对路径.若使用则会根据不同的FS驱动产生不同的结果 )
When opening files, FS driver will only be given relative path to files. For example:
打开文件时,FS驱动程序将只被赋予文件的相对路径。例如:
myfs
driver is registered with/data
as path prefix- and application calls
fopen("/data/config.json", ...)
- then VFS component will call
myfs_open("/config.json", ...)
. myfs
driver will open/config.json
filemyfs
驱动程序注册/data
为路径前缀- 和应用程序调用
fopen("/data/config.json", ...)
- 那么VFS组件将会调用。
myfs_open("/config.json", ...)
myfs
驱动程序将打开/config.json
文件
(根据/data选择实际挂载点,然后调用相应的FS实例的接口)
VFS doesn’t impose a limit on total file path length, but it does limit FS path prefix to ESP_VFS_PATH_MAX
characters. Individual FS drivers may have their own filename length limitations.
VFS不会对文件路径总长度施加限制,但它会将FS路径前缀限制为ESP_VFS_PATH_MAX
个字符。个别FS驱动程序可能有自己的文件名长度限制。
文件描述符
It is suggested that filesystem drivers should use small positive integers as file descriptors. VFS component assumes that CONFIG_MAX_FD_BITS
bits (12 by default) are sufficient to represent a file descriptor.
建议文件系统驱动程序应该使用小的正整数作为文件描述符。VFS组件假定CONFIG_MAX_FD_BITS
位(默认值为12)足以表示文件描述符。
While file descriptors returned by VFS component to newlib library are rarely seen by the application, the following details may be useful for debugging purposes. File descriptors returned by VFS component are composed of two parts: FS driver ID, and the actual file descriptor. Because newlib stores file descriptors as 16-bit integers, VFS component is also limited by 16 bits to store both parts.
尽管应用程序很少看到VFS组件返回到newlib库的文件描述符,但以下详细信息对于调试可能很有用。由VFS组件返回的文件描述符由两部分组成:FS驱动程序ID和实际的文件描述符。由于newlib将文件描述符存储为16位整数,因此VFS组件也受16位限制以存储这两个部分。
Lower CONFIG_MAX_FD_BITS
bits are used to store zero-based file descriptor. The per-filesystem FD obtained from the FS open
call, and this result is stored in the lower bits of the FD. Higher bits are used to save the index of FS in the internal table of registered filesystems.
低位CONFIG_MAX_FD_BITS
用于存储基于零的文件描述符。调用FS的open
函数获得的每个文件系统FD(文件描述符),并且该结果存储在FD的较低位中。高位用于将已注册文件系统中FS的索引保存在内部表(internal table)中。
When VFS component receives a call from newlib which has a file descriptor, this file descriptor is translated back to the FS-specific file descriptor. First, higher bits of FD are used to identify the FS. Then only the lower CONFIG_MAX_FD_BITS
bits of the fd are masked in, and resulting FD is passed to the FS driver.
当VFS组件从具有文件描述符的newlib接受调用时,此文件描述符将为FS特定的文件描述符。首先,FD的较高位用于识别FS。然后,只有fd的较低位CONFIG_MAX_FD_BITS
被屏蔽,并将生成的FD传递给FS驱动程序。
FD as seen by newlib FD as seen by FS driver
+-------+---------------+ +------------------------+
| FS id | Zero—based FD | +--------------------------> |
+---+---+------+--------+ | +------------------------+
| | |
| +--------------+
|
| +-------------+
| | Table of |
| | registered |
| | filesystems |
| +-------------+ +-------------+
+-------> entry +----> esp_vfs_t |
index +-------------+ | structure |
| | | |
| | | |
+-------------+ +-------------+
标准IO流(stdin, stdout, stderr)
If “UART for console output” menuconfig option is not set to “None”, then stdin
, stdout
, and stderr
are configured to read from, and write to, a UART. It is possible to use UART0 or UART1 for standard IO. By default, UART0 is used, with 115200 baud rate, TX pin is GPIO1 and RX pin is GPIO3. These parameters can be changed in menuconfig.
如果menuconfig的选项“UART for console output”未设置为“None”,那么stdin
,stdout
和stderr
被配置为向UART读取和写入。标准IO可以使用UART0或UART1。默认情况下,使用UART0,波特率为115200,TX引脚为GPIO1,RX引脚为GPIO3。这些参数可以在menuconfig中更改。
Writing to stdout
or stderr
will send characters to the UART transmit FIFO. Reading from stdin
will retrieve characters from the UART receive FIFO.
写入stdout
或stderr
将发送字符到UART发送FIFO。从stdin
读取将UART接收FIFO中检索字符。
By default, VFS uses simple functions for reading from and writing to UART. Writes busy-wait until all data is put into UART FIFO, and reads are non-blocking, returning only the data present in the FIFO. Because of this non-blocking read behavior, higher level C library calls, such as fscanf("%d\n", &var);
may not have desired results.
默认情况下,VFS使用简单的函数来读取和写入UART。写入忙等待,直到所有数据被放入UART FIFO中,并且读取是非阻塞的,只返回FIFO中的数据。由于这种非阻塞式读取行为,更高级别的C库调用,例如fscanf("%d\n", &var);
可能不具有期望的结果。
Applications which use UART driver may instruct VFS to use the driver’s interrupt driven, blocking read and write functions instead. This can be done using a call to esp_vfs_dev_uart_use_driver
function. It is also possible to revert to the basic non-blocking functions using a call to esp_vfs_dev_uart_use_nonblocking
.
使用UART驱动程序的应用程序可能会指示VFS使用驱动程序的中断驱动程序,而不是使用读取和写入功能。这可以通过调用esp_vfs_dev_uart_use_driver
函数来完成。也可以使用esp_vfs_dev_uart_use_nonblocking
调用返回到基本的非阻塞函数。
VFS also provides optional newline conversion feature for input and output. Internally, most applications send and receive lines terminated by LF (‘’n’‘) character. Different terminal programs may require different line termination, such as CR or CRLF. Applications can configure this separately for input and output either via menuconfig, or by calls to esp_vfs_dev_uart_set_rx_line_endings
and esp_vfs_dev_uart_set_tx_line_endings
functions.
VFS还为输入和输出提供可选的换行转换功能。在内部,大多数应用程序发送和接收由LF(‘’n’‘)作为换行符。不同的终端程序可能需要不同的换行符,如CR或CRLF。应用程序可以通过menuconfig或通过调用esp_vfs_dev_uart_set_rx_line_endings
和esp_vfs_dev_uart_set_tx_line_endings
函数单独配置输入和输出。
标准流和FreeRTOS任务
FILE
objects for stdin
, stdout
, and stderr
are shared between all FreeRTOS tasks, but the pointers to these objects are are stored in per-task struct _reent
. The following code:
stdin
,stdout
和stderr
的FILE
对象在所有FreeRTOS的任务间是共享的,但指向这些对象的指针存储在每个任务的struct _reent
中。以下代码:
fprintf(stderr, "42\n");
actually is translated to to this (by the preprocessor):
实际上被翻译成这个(由预处理器):
fprintf(__getreent()->_stderr, "42\n");
where the __getreent()
function returns a per-task pointer to struct _reent
(newlib/include/sys/reent.h#L370-L417). This structure is allocated on the TCB of each task. When a task is initialized, _stdin
, _stdout
and _stderr
members of struct _reent
are set to the values of _stdin
, _stdout
and _stderr
of _GLOBAL_REENT
(i.e. the structure which is used before FreeRTOS is started).
其中__getreent()
函数返回每个任务的struct _reent
指针(newlib/include/sys/reent.h#L370-L417)。这个结构被分配在每个任务的TCB上。当一个任务已初始化,struct _reent
的_stdin
, _stdout
和 _stderr
成员被设置为_GLOBAL_REENT
的_stdin
, _stdout
和 _stderr
,和的(即其被FreeRTOS的之前使用的结构启动)。
Such a design has the following consequences:
这样的设计具有以下结果:
- It is possible to set
stdin
,stdout
, andstderr
for any given task without affecting other tasks, e.g. by doingstdin = fopen("/dev/uart/1", "r")
. - Closing default
stdin
,stdout
, orstderr
usingfclose
will close theFILE
stream object — this will affect all other tasks. To change the default
stdin
,stdout
,stderr
streams for new tasks, modify_GLOBAL_REENT->_stdin
(_stdout
,_stderr
) before creating the task.对于任何给定的任务,它可以设置
stdin
,stdout
以及stderr
为不影响其他任务的,例如,通过stdin = fopen("/dev/uart/1", "r")
- 关闭默认值
stdin
,stdout
或stderr
使用fclose
将关闭FILE
流对象 - 这将影响所有其他任务。 - 要为新任务更改默认的
stdin
,stdout
,stderr
流,需要在创建任务之前修改_GLOBAL_REENT->_stdin
(_stdout
,_stderr
).
应用示例
API参考
头文件
本文翻译自:https://esp-idf.readthedocs.io/en/latest/api-reference/storage/vfs.html
翻译水平有限,如有错误欢迎指正