虚拟文件系统组件

[English]

概述

虚拟文件系统 (VFS) 组件可为一些驱动提供一个统一接口。有了该接口,用户可像操作普通文件一样操作虚拟文件。这类驱动程序可以是 FAT、SPIFFS 等真实文件系统,也可以是有文件类接口的设备驱动程序。

VFS 组件支持 C 库函数(如 fopen 和 fprintf 等)与文件系统 (FS) 驱动程序协同工作。在高层级,每个 FS 驱动程序均与某些路径前缀相关联。当一个 C 库函数需要打开文件时,VFS 组件将搜索与该文件所在文件路径相关联的 FS 驱动程序,并将调用传递给该驱动程序。针对该文件的读取、写入等其他操作的调用也将传递给这个驱动程序。

例如,您可以使用 /fat 前缀注册 FAT 文件系统驱动,之后即可调用 fopen("/fat/file.txt", "w")。之后,VFS 将调用 FAT 驱动的 open 函数,并将参数 /file.txt 和合适的打开模式传递给 open 函数;后续对返回的 FILE* 数据流调用 C 库函数也同样会传递给 FAT 驱动。

注册 FS 驱动程序

如需注册 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));

在上述代码中需要用到 readwriteread_pwrite_p,具体使用哪组函数由 FS 驱动程序 API 的声明方式决定。

示例 1:声明 API 函数时不带额外的上下文指针参数,即 FS 驱动程序为单例模式,此时使用 write

ssize_t myfs_write(int fd, const void * data, size_t size);

// In definition of esp_vfs_t:
    .flags = ESP_VFS_FLAG_DEFAULT,
    .write = &myfs_write,
// ... other members initialized

// When registering FS, context pointer (third argument) is NULL:
ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));

示例 2:声明 API 函数时需要一个额外的上下文指针作为参数,即可支持多个 FS 驱动程序实例,此时使用 write_p

ssize_t myfs_write(myfs_t* fs, int fd, const void * data, size_t size);

// In definition of 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)
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));

同步输入/输出多路复用

如需通过 select() 使用同步输入/输出多路复用,首先需要把 start_select()end_select() 注册到 VFS,如下所示:

// In definition of esp_vfs_t:
    .start_select = &uart_start_select,
    .end_select = &uart_end_select,
// ... other members initialized

调用 start_select() 设置环境,用以检测某一 VFS 文件描述符的读取/写入/错误条件。调用 end_select() 终止、析构或释放 start_select() 设置的资源。请在 vfs/vfs_uart.c 中查看 UART 外设参考实现、esp_vfs_dev_uart_register()uart_start_select()uart_end_select() 函数。

请参考以下示例,查看如何使用 VFS 文件描述符调用 select()

如果 select() 用于套接字文件描述符,您可以启用 CONFIG_LWIP_USE_ONLY_LWIP_SELECT 选项来减少代码量,提高性能。

路径

已注册的 FS 驱动程序均有一个路径前缀与之关联,此路径前缀即为分区的挂载点。

如果挂载点中嵌套了其他挂载点,则在打开文件时使用具有最长匹配路径前缀的挂载点。例如,假设以下文件系统已在 VFS 中注册:

  • 在 /data 下注册 FS 驱动程序 1
  • 在 /data/static 下注册 FS 驱动程序 2

那么:

  • 打开 /data/log.txt 会调用驱动程序 FS 1;
  • 打开 /data/static/index.html 需调用 FS 驱动程序 2;
  • 即便 FS 驱动程序 2 中没有 /index.html,也不会在 FS 驱动程序 1 中查找 /static/index.html

挂载点名称必须以路径分隔符 (/) 开头,且分隔符后至少包含一个字符。但在以下情况中,VFS 同样支持空的挂载点名称:1. 应用程序需要提供一个”最后方案“下使用的文件系统;2. 应用程序需要同时覆盖 VFS 功能。如果没有与路径匹配的前缀,就会使用到这种文件系统。

VFS 不会对路径中的点 (.) 进行特殊处理,也不会将 .. 视为对父目录的引用。在上述示例中,使用 /data/static/../log.txt 路径不会调用 FS 驱动程序 1 打开 /log.txt。特定的 FS 驱动程序(如 FATFS)可能以不同的方式处理文件名中的点。

执行打开文件操作时,FS 驱动程序仅得到文件的相对路径(挂载点前缀已经被去除):

  1. /data 为路径前缀注册 myfs 驱动;
  2. 应用程序调用 fopen("/data/config.json", ...)
  3. VFS 调用 myfs_open("/config.json", ...)
  4. myfs 驱动打开 /config.json 文件。

VFS 对文件路径长度没有限制,但文件系统路径前缀受 ESP_VFS_PATH_MAX 限制,即路径前缀上限为 ESP_VFS_PATH_MAX。各个文件系统驱动则可能会对自己的文件名长度设置一些限制。

文件描述符

文件描述符是一组很小的正整数,从 0FD_SETSIZE - 1FD_SETSIZE 在 newlib sys/types.h 中定义。最大文件描述符由 CONFIG_LWIP_MAX_SOCKETS 定义,且为套接字保留。VFS 中包含一个名为 s_fd_table 的查找表,用于将全局文件描述符映射至 s_vfs 数组中注册的 VFS 驱动索引。

标准 IO 流 (stdin, stdout, stderr)

如果 menuconfig 中 UART for console output 选项没有设置为 None,则 stdinstdoutstderr 将默认从 UART 读取或写入。UART0 或 UART1 可用作标准 IO。默认情况下,UART0 使用 115200 波特率,TX 管脚为 GPIO1,RX 管脚为 GPIO3。您可以在 menuconfig 中更改上述参数。

stdoutstderr 执行写入操作将会向 UART 发送 FIFO 发送字符,对 stdin 执行读取操作则会从 UART 接收 FIFO 中取出字符。

默认情况下,VFS 使用简单的函数对 UART 进行读写操作。在所有数据放进 UART FIFO 之前,写操作将处于 busy-wait 状态,读操处于非阻塞状态,仅返回 FIFO 中已有数据。由于读操作为非阻塞,高层级 C 库函数调用(如 fscanf("%d\n", &var);)可能获取不到所需结果。

如果应用程序使用 UART 驱动,则可以调用 esp_vfs_dev_uart_use_driver 函数来指导 VFS 使用驱动中断、读写阻塞功能等。您也可以调用 esp_vfs_dev_uart_use_nonblocking 来恢复非阻塞函数。

VFS 还为输入和输出提供换行符转换功能(可选)。多数应用程序在程序内部发送或接收以 LF (‘’n’‘) 结尾的行,但不同的终端程序可能需要不同的换行符,比如 CR 或 CRLF。应用程序可以通过 menuconfig 或者调用 esp_vfs_dev_uart_set_rx_line_endingsesp_vfs_dev_uart_set_tx_line_endings 为输入输出配置换行符。

标准流和 FreeRTOS 任务

stdinstdoutstderrFILE 对象在所有 FreeRTOS 任务之间共享,指向这些对象的指针分别存储在每个任务的 struct _reent 中。

预处理器把如下代码:

fprintf(stderr, "42\n");

解释为:

fprintf(__getreent()->_stderr, "42\n");

其中 __getreent() 函数将为每个任务返回一个指向 struct _reent 的指针 (newlib/include/sys/reent.h#L370-L417)。每个任务的 TCB 均拥有一个 struct _reent 结构体,任务初始化后,struct _reent 结构体中的 _stdin_stdout_stderr 将会被赋予 _GLOBAL_REENT_stdin_stdout_stderr 的值,_GLOBAL_REENT 即为 FreeRTOS 启动之前所用结构体。

这样设计带来的结果是:

  • 允许重定向给定任务的 stdinstdoutstderr,而不影响其他任务,例如通过 stdin = fopen("/dev/uart/1", "r")
  • 但使用 fclose 关闭默认 stdinstdoutstderr 将同时关闭相应的 FILE 流对象,因此会影响其他任务;
  • 如需更改新任务的默认 stdinstdoutstderr 流,请在创建新任务之前修改 _GLOBAL_REENT->_stdin (_stdout_stderr)。

应用示例

指南 (未完成)

API 参考

Functions

ssize_t esp_vfs_write(struct _reent *r, int fd, const void *data, size_t size)

These functions are to be used in newlib syscall table. They will be called by newlib when it needs to use any of the syscalls.

off_t esp_vfs_lseek(struct _reent *r, int fd, off_t size, int mode)
ssize_t esp_vfs_read(struct _reent *r, int fd, void *dst, size_t size)
int esp_vfs_open(struct _reent *r, const char *path, int flags, int mode)
int esp_vfs_close(struct _reent *r, int fd)
int esp_vfs_fstat(struct _reent *r, int fd, struct stat *st)
int esp_vfs_stat(struct _reent *r, const char *path, struct stat *st)
int esp_vfs_link(struct _reent *r, const char *n1, const char *n2)
int esp_vfs_unlink(struct _reent *r, const char *path)
int esp_vfs_rename(struct _reent *r, const char *src, const char *dst)
int esp_vfs_utime(const char *path, const struct utimbuf *times)
esp_err_t esp_vfs_register(const char *base_path, const esp_vfs_t *vfs, void *ctx)

Register a virtual filesystem for given path prefix.

Return
ESP_OK if successful, ESP_ERR_NO_MEM if too many VFSes are registered.
Parameters
  • base_path: file path prefix associated with the filesystem. Must be a zero-terminated C string, up to ESP_VFS_PATH_MAX characters long, and at least 2 characters long. Name must start with a “/” and must not end with “/”. For example, “/data” or “/dev/spi” are valid. These VFSes would then be called to handle file paths such as “/data/myfile.txt” or “/dev/spi/0”.
  • vfs: Pointer to esp_vfs_t, a structure which maps syscalls to the filesystem driver functions. VFS component doesn’t assume ownership of this pointer.
  • ctx: If vfs->flags has ESP_VFS_FLAG_CONTEXT_PTR set, a pointer which should be passed to VFS functions. Otherwise, NULL.

esp_err_t esp_vfs_register_fd_range(const esp_vfs_t *vfs, void *ctx, int min_fd, int max_fd)

Special case function for registering a VFS that uses a method other than open() to open new file descriptors from the interval <min_fd; max_fd).

This is a special-purpose function intended for registering LWIP sockets to VFS.

Return
ESP_OK if successful, ESP_ERR_NO_MEM if too many VFSes are registered, ESP_ERR_INVALID_ARG if the file descriptor boundaries are incorrect.
Parameters
  • vfs: Pointer to esp_vfs_t. Meaning is the same as for esp_vfs_register().
  • ctx: Pointer to context structure. Meaning is the same as for esp_vfs_register().
  • min_fd: The smallest file descriptor this VFS will use.
  • max_fd: Upper boundary for file descriptors this VFS will use (the biggest file descriptor plus one).

esp_err_t esp_vfs_register_with_id(const esp_vfs_t *vfs, void *ctx, esp_vfs_id_t *vfs_id)

Special case function for registering a VFS that uses a method other than open() to open new file descriptors. In comparison with esp_vfs_register_fd_range, this function doesn’t pre-registers an interval of file descriptors. File descriptors can be registered later, by using esp_vfs_register_fd.

Return
ESP_OK if successful, ESP_ERR_NO_MEM if too many VFSes are registered, ESP_ERR_INVALID_ARG if the file descriptor boundaries are incorrect.
Parameters
  • vfs: Pointer to esp_vfs_t. Meaning is the same as for esp_vfs_register().
  • ctx: Pointer to context structure. Meaning is the same as for esp_vfs_register().
  • vfs_id: Here will be written the VFS ID which can be passed to esp_vfs_register_fd for registering file descriptors.

esp_err_t esp_vfs_unregister(const char *base_path)

Unregister a virtual filesystem for given path prefix

Return
ESP_OK if successful, ESP_ERR_INVALID_STATE if VFS for given prefix hasn’t been registered
Parameters
  • base_path: file prefix previously used in esp_vfs_register call

esp_err_t esp_vfs_register_fd(esp_vfs_id_t vfs_id, int *fd)

Special function for registering another file descriptor for a VFS registered by esp_vfs_register_with_id.

Return
ESP_OK if the registration is successful, ESP_ERR_NO_MEM if too many file descriptors are registered, ESP_ERR_INVALID_ARG if the arguments are incorrect.
Parameters
  • vfs_id: VFS identificator returned by esp_vfs_register_with_id.
  • fd: The registered file descriptor will be written to this address.

esp_err_t esp_vfs_unregister_fd(esp_vfs_id_t vfs_id, int fd)

Special function for unregistering a file descriptor belonging to a VFS registered by esp_vfs_register_with_id.

Return
ESP_OK if the registration is successful, ESP_ERR_INVALID_ARG if the arguments are incorrect.
Parameters
  • vfs_id: VFS identificator returned by esp_vfs_register_with_id.
  • fd: File descriptor which should be unregistered.

int esp_vfs_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)

Synchronous I/O multiplexing which implements the functionality of POSIX select() for VFS.

Return
The number of descriptors set in the descriptor sets, or -1 when an error (specified by errno) have occurred.
Parameters
  • nfds: Specifies the range of descriptors which should be checked. The first nfds descriptors will be checked in each set.
  • readfds: If not NULL, then points to a descriptor set that on input specifies which descriptors should be checked for being ready to read, and on output indicates which descriptors are ready to read.
  • writefds: If not NULL, then points to a descriptor set that on input specifies which descriptors should be checked for being ready to write, and on output indicates which descriptors are ready to write.
  • errorfds: If not NULL, then points to a descriptor set that on input specifies which descriptors should be checked for error conditions, and on output indicates which descriptors have error conditions.
  • timeout: If not NULL, then points to timeval structure which specifies the time period after which the functions should time-out and return. If it is NULL, then the function will not time-out.

void esp_vfs_select_triggered(esp_vfs_select_sem_t sem)

Notification from a VFS driver about a read/write/error condition.

This function is called when the VFS driver detects a read/write/error condition as it was requested by the previous call to start_select.

Parameters
  • sem: semaphore structure which was passed to the driver by the start_select call

void esp_vfs_select_triggered_isr(esp_vfs_select_sem_t sem, BaseType_t *woken)

Notification from a VFS driver about a read/write/error condition (ISR version)

This function is called when the VFS driver detects a read/write/error condition as it was requested by the previous call to start_select.

Parameters
  • sem: semaphore structure which was passed to the driver by the start_select call
  • woken: is set to pdTRUE if the function wakes up a task with higher priority

int esp_vfs_poll(struct pollfd *fds, nfds_t nfds, int timeout)

Implements the VFS layer for synchronous I/O multiplexing by poll()

The implementation is based on esp_vfs_select. The parameters and return values are compatible with POSIX poll().

Return
A positive return value indicates the number of file descriptors that have been selected. The 0 return value indicates a timed-out poll. -1 is return on failure and errno is set accordingly.
Parameters
  • fds: Pointer to the array containing file descriptors and events poll() should consider.
  • nfds: Number of items in the array fds.
  • timeout: Poll() should wait at least timeout milliseconds. If the value is 0 then it should return immediately. If the value is -1 then it should wait (block) until the event occurs.

ssize_t esp_vfs_pread(int fd, void *dst, size_t size, off_t offset)

Implements the VFS layer of POSIX pread()

Return
A positive return value indicates the number of bytes read. -1 is return on failure and errno is set accordingly.
Parameters
  • fd: File descriptor used for read
  • dst: Pointer to the buffer where the output will be written
  • size: Number of bytes to be read
  • offset: Starting offset of the read

ssize_t esp_vfs_pwrite(int fd, const void *src, size_t size, off_t offset)

Implements the VFS layer of POSIX pwrite()

Return
A positive return value indicates the number of bytes written. -1 is return on failure and errno is set accordingly.
Parameters
  • fd: File descriptor used for write
  • src: Pointer to the buffer from where the output will be read
  • size: Number of bytes to write
  • offset: Starting offset of the write

Structures

struct esp_vfs_select_sem_t

VFS semaphore type for select()

Public Members

bool is_sem_local

type of “sem” is SemaphoreHandle_t when true, defined by socket driver otherwise

void *sem

semaphore instance

struct esp_vfs_t

VFS definition structure.

This structure should be filled with pointers to corresponding FS driver functions.

VFS component will translate all FDs so that the filesystem implementation sees them starting at zero. The caller sees a global FD which is prefixed with an pre-filesystem-implementation.

Some FS implementations expect some state (e.g. pointer to some structure) to be passed in as a first argument. For these implementations, populate the members of this structure which have _p suffix, set flags member to ESP_VFS_FLAG_CONTEXT_PTR and provide the context pointer to esp_vfs_register function. If the implementation doesn’t use this extra argument, populate the members without _p suffix and set flags member to ESP_VFS_FLAG_DEFAULT.

If the FS driver doesn’t provide some of the functions, set corresponding members to NULL.

Public Members

int flags

ESP_VFS_FLAG_CONTEXT_PTR or ESP_VFS_FLAG_DEFAULT

esp_err_t (*start_select)(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, esp_vfs_select_sem_t sem, void **end_select_args)

start_select is called for setting up synchronous I/O multiplexing of the desired file descriptors in the given VFS

int (*socket_select)(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)

socket select function for socket FDs with the functionality of POSIX select(); this should be set only for the socket VFS

void (*stop_socket_select)(void *sem)

called by VFS to interrupt the socket_select call when select is activated from a non-socket VFS driver; set only for the socket driver

void (*stop_socket_select_isr)(void *sem, BaseType_t *woken)

stop_socket_select which can be called from ISR; set only for the socket driver

void *(*get_socket_select_semaphore)(void)

end_select is called to stop the I/O multiplexing and deinitialize the environment created by start_select for the given VFS

esp_err_t (*end_select)(void *end_select_args)

get_socket_select_semaphore returns semaphore allocated in the socket driver; set only for the socket driver

Macros

MAX_FDS

Maximum number of (global) file descriptors.

ESP_VFS_PATH_MAX

Maximum length of path prefix (not including zero terminator)

ESP_VFS_FLAG_DEFAULT

Default value of flags member in esp_vfs_t structure.

ESP_VFS_FLAG_CONTEXT_PTR

Flag which indicates that FS needs extra context pointer in syscalls.

Type Definitions

typedef int esp_vfs_id_t

Functions

void esp_vfs_dev_uart_register(void)

add /dev/uart virtual filesystem driver

This function is called from startup code to enable serial output

void esp_vfs_dev_uart_set_rx_line_endings(esp_line_endings_t mode)

Set the line endings expected to be received on UART.

This specifies the conversion between line endings received on UART and newlines (‘

’, LF) passed into stdin:

  • ESP_LINE_ENDINGS_CRLF: convert CRLF to LF
  • ESP_LINE_ENDINGS_CR: convert CR to LF
  • ESP_LINE_ENDINGS_LF: no modification

Note
this function is not thread safe w.r.t. reading from UART
Parameters
  • mode: line endings expected on UART

void esp_vfs_dev_uart_set_tx_line_endings(esp_line_endings_t mode)

Set the line endings to sent to UART.

This specifies the conversion between newlines (‘

’, LF) on stdout and line endings sent over UART:

  • ESP_LINE_ENDINGS_CRLF: convert LF to CRLF
  • ESP_LINE_ENDINGS_CR: convert LF to CR
  • ESP_LINE_ENDINGS_LF: no modification

Note
this function is not thread safe w.r.t. writing to UART
Parameters
  • mode: line endings to send to UART

void esp_vfs_dev_uart_use_nonblocking(int uart_num)

set VFS to use simple functions for reading and writing UART Read is non-blocking, write is busy waiting until TX FIFO has enough space. These functions are used by default.

Parameters
  • uart_num: UART peripheral number

void esp_vfs_dev_uart_use_driver(int uart_num)

set VFS to use UART driver for reading and writing

Note
application must configure UART driver before calling these functions With these functions, read and write are blocking and interrupt-driven.
Parameters
  • uart_num: UART peripheral number

Enumerations

enum esp_line_endings_t

Line ending settings.

Values:

ESP_LINE_ENDINGS_CRLF

CR + LF.

ESP_LINE_ENDINGS_CR

CR.

ESP_LINE_ENDINGS_LF

LF.