Merge branch 'doc/size_opt_vfs' into 'master'

docs: describe code size optimizations possible in VFS component

See merge request espressif/esp-idf!14783
This commit is contained in:
Ivan Grokhotkov 2021-08-23 07:32:14 +00:00
commit 6cf25ce43f
6 changed files with 432 additions and 417 deletions

View File

@ -14,6 +14,11 @@ menu "Virtual file system"
Disabling this option can save memory when the support for these functions
is not required.
Note that the following functions can still be used with socket file descriptors
when this option is disabled:
close, read, write, ioctl, fcntl.
config VFS_SUPPORT_DIR
bool "Provide directory related functions"
default y

View File

@ -1,248 +0,0 @@
Virtual filesystem component
============================
:link_to_translation:`zh_CN:[中文]`
Overview
--------
Virtual filesystem (VFS) component provides a unified interface for drivers which can perform operations on file-like objects. These can be real filesystems (FAT, SPIFFS, etc.) or device drivers which provide a file-like interface.
This component allows C library functions, such as fopen and fprintf, to work with FS drivers. At a high level, each FS driver is associated with some path prefix. When one of C library functions needs to open a file, the VFS component searches for the FS driver associated with the file 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.
For example, one can register a FAT filesystem driver with the ``/fat`` prefix and call ``fopen("/fat/file.txt", "w")``. The VFS component will then call the function ``open`` of the FAT driver and pass the argument ``/file.txt`` to it together with appropriate mode flags. All subsequent calls to C library functions for the returned ``FILE*`` stream will also be forwarded to the FAT driver.
FS registration
---------------
To register an FS driver, an application needs to define an instance of the :cpp:type:`esp_vfs_t` structure and populate it with function pointers to FS APIs:
.. highlight:: c
::
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 how the FS driver declares its API functions, either ``read``, ``write``, etc., or ``read_p``, ``write_p``, etc., should be used.
Case 1: API functions are declared without an extra context pointer (the FS driver is a singleton)::
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));
Case 2: API functions are declared with an extra context pointer (the FS driver supports multiple instances)::
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));
Synchronous input/output multiplexing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Synchronous input/output multiplexing by :cpp:func:`select` is supported in the VFS component. The implementation
works in the following way.
1. :cpp:func:`select` is called with file descriptors which could belong to various VFS drivers.
2. The file descriptors are divided into groups each belonging to one VFS driver.
3. The file descriptors belonging to non-socket VFS drivers are handed over to the given VFS drivers by :cpp:func:`start_select`
described later on this page. This function represents the driver-specific implementation of :cpp:func:`select` for
the given driver. This should be a non-blocking call which means the function should immediately return after setting up
the environment for checking events related to the given file descriptors.
4. The file descriptors belonging to the socket VFS driver are handed over to the socket driver by
:cpp:func:`socket_select` described later on this page. This is a blocking call which means that it will return only
if there is an event related to socket file descriptors or a non-socket driver signals :cpp:func:`socket_select`
to exit.
5. Results are collected from each VFS driver and all drivers are stopped by deinitiazation
of the environment for checking events.
6. The :cpp:func:`select` call ends and returns the appropriate results.
Non-socket VFS drivers
""""""""""""""""""""""
If you want to use :cpp:func:`select` with a file descriptor belonging to a non-socket VFS driver
then you need to register the driver with functions :cpp:func:`start_select` and
:cpp:func:`end_select` similarly to the following example:
.. highlight:: c
::
// In definition of esp_vfs_t:
.start_select = &uart_start_select,
.end_select = &uart_end_select,
// ... other members initialized
:cpp:func:`start_select` is called for setting up the environment for
detection of read/write/error conditions on file descriptors belonging to the
given VFS driver.
:cpp:func:`end_select` is called to stop/deinitialize/free the
environment which was setup by :cpp:func:`start_select`.
.. note::
:cpp:func:`end_select` might be called without a previous :cpp:func:`start_select` call in some rare
circumstances. :cpp:func:`end_select` should fail gracefully if this is the case.
Please refer to the
reference implementation for the UART peripheral in
:component_file:`vfs/vfs_uart.c` and most particularly to the functions
:cpp:func:`esp_vfs_dev_uart_register`, :cpp:func:`uart_start_select`, and
:cpp:func:`uart_end_select` for more information.
Please check the following examples that demonstrate the use of :cpp:func:`select` with VFS file descriptors:
- :example:`peripherals/uart/uart_select`
- :example:`system/select`
Socket VFS drivers
""""""""""""""""""
A socket VFS driver is using its own internal implementation of :cpp:func:`select` and non-socket VFS drivers notify
it upon read/write/error conditions.
A socket VFS driver needs to be registered with the following functions defined:
.. highlight:: c
::
// In definition of esp_vfs_t:
.socket_select = &lwip_select,
.get_socket_select_semaphore = &lwip_get_socket_select_semaphore,
.stop_socket_select = &lwip_stop_socket_select,
.stop_socket_select_isr = &lwip_stop_socket_select_isr,
// ... other members initialized
:cpp:func:`socket_select` is the internal implementation of :cpp:func:`select` for the socket driver. It works only
with file descriptors belonging to the socket VFS.
:cpp:func:`get_socket_select_semaphore` returns the signalization object (semaphore) which will be used in non-socket
drivers to stop the waiting in :cpp:func:`socket_select`.
:cpp:func:`stop_socket_select` call is used to stop the waiting in :cpp:func:`socket_select` by passing the object
returned by :cpp:func:`get_socket_select_semaphore`.
:cpp:func:`stop_socket_select_isr` has the same functionality as :cpp:func:`stop_socket_select` but it can be used
from ISR.
Please see :component_file:`lwip/port/esp32/vfs_lwip.c` for a reference socket driver implementation using LWIP.
.. note::
If you use :cpp:func:`select` for socket file descriptors only then you can enable the
:envvar:`CONFIG_LWIP_USE_ONLY_LWIP_SELECT` option to reduce the code size and improve performance.
.. note::
Don't change the socket driver during an active :cpp:func:`select` call or you might experience some undefined
behavior.
Paths
-----
Each registered FS has a path prefix associated with it. This prefix can be considered as a "mount point" of this partition.
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:
- FS 1 on /data
- FS 2 on /data/static
Then:
- 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"`` does not exist in FS 2, FS 1 will *not* be searched for ``/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 might be used in cases when an application needs to provide a "fallback" filesystem or to override VFS functionality altogether. Such filesystem will be used if no prefix matches the path given.
VFS does not handle dots (``.``) in path names in any special way. VFS does not treat ``..`` as a reference to the parent directory. 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) might handle dots in file names differently.
When opening files, the FS driver receives only relative paths to files. For example:
1. The ``myfs`` driver is registered with ``/data`` as a path prefix.
2. The application calls ``fopen("/data/config.json", ...)``.
3. The VFS component calls ``myfs_open("/config.json", ...)``.
4. The ``myfs`` driver opens the ``/config.json`` file.
VFS does not impose any limit on total file path length, but it does limit the FS path prefix to ``ESP_VFS_PATH_MAX`` characters. Individual FS drivers may have their own filename length limitations.
File descriptors
----------------
File descriptors are small positive integers from ``0`` to ``FD_SETSIZE - 1``, where ``FD_SETSIZE`` is defined in newlib's ``sys/types.h``. The largest file descriptors (configured by ``CONFIG_LWIP_MAX_SOCKETS``) are reserved for sockets. The VFS component contains a lookup-table called ``s_fd_table`` for mapping global file descriptors to VFS driver indexes registered in the ``s_vfs`` array.
Standard IO streams (stdin, stdout, stderr)
-------------------------------------------
If the menuconfig option ``UART for console output`` 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; RX pin is GPIO3. These parameters can be changed in 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.
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. Due to this non-blocking read behavior, higher level C library calls, such as ``fscanf("%d\n", &var);``, might not have desired results.
Applications which use the UART driver can instruct VFS to use the driver's interrupt driven, blocking read and write functions instead. This can be done using a call to the ``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``.
VFS also provides an optional newline conversion feature for input and output. Internally, most applications send and receive lines terminated by the 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 the functions ``esp_vfs_dev_uart_port_set_rx_line_endings`` and ``esp_vfs_dev_uart_port_set_tx_line_endings``.
Standard streams and FreeRTOS tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``FILE`` objects for ``stdin``, ``stdout``, and ``stderr`` are shared between all FreeRTOS tasks, but the pointers to these objects are stored in per-task ``struct _reent``.
The following code is transferred to ``fprintf(__getreent()->_stderr, "42\n");`` by the preprocessor:
.. highlight:: c
::
fprintf(stderr, "42\n");
The ``__getreent()`` function returns a per-task pointer to ``struct _reent`` in newlib libc. 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).
Such a design has the following consequences:
- It is possible to set ``stdin``, ``stdout``, and ``stderr`` for any given task without affecting other tasks, e.g., by doing ``stdin = fopen("/dev/uart/1", "r")``.
- Closing default ``stdin``, ``stdout``, or ``stderr`` using ``fclose`` will close the ``FILE`` stream object, which 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.
Event fds
-------------------------------------------
``eventfd()`` call is a powerful tool to notify a ``select()`` based loop of custom events. The ``eventfd()`` implementation in ESP-IDF is generally the same as described in ``man(2) eventfd`` except for:
- ``esp_vfs_eventfd_register()`` has to be called before calling ``eventfd()``
- Options ``EFD_CLOEXEC``, ``EFD_NONBLOCK`` and ``EFD_SEMAPHORE`` are not supported in flags.
- Option ``EFD_SUPPORT_ISR`` has been added in flags. This flag is required to read and the write the eventfd in an interrupt handler.
Note that creating an eventfd with ``EFD_SUPPORT_ISR`` will cause interrupts to be temporarily disabled when reading, writing the file and during the beginning and the ending of the ``select()`` when this file is set.

View File

@ -1,167 +0,0 @@
虚拟文件系统组件
============================
:link_to_translation:`en:[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 驱动程序,首先要定义一个 :cpp:type:`esp_vfs_t` 结构体实例,并用指向 FS API 的函数指针填充它。
.. highlight:: c
::
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));
在上述代码中需要用到 ``read````write````read_p````write_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));
同步输入/输出多路复用
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
如需通过 :cpp:func:`select` 使用同步输入/输出多路复用,首先需要把 :cpp:func:`start_select`:cpp:func:`end_select` 注册到 VFS如下所示
.. highlight:: c
::
// In definition of esp_vfs_t:
.start_select = &uart_start_select,
.end_select = &uart_end_select,
// ... other members initialized
调用 :cpp:func:`start_select` 设置环境,用以检测某一 VFS 文件描述符的读取/写入/错误条件。调用 :cpp:func:`end_select` 终止、析构或释放 :cpp:func:`start_select` 设置的资源。请在 :component_file:`vfs/vfs_uart.c` 中查看 UART 外设参考实现、:cpp:func:`esp_vfs_dev_uart_register`:cpp:func:`uart_start_select`:cpp:func:`uart_end_select` 函数。
请参考以下示例,查看如何使用 VFS 文件描述符调用 :cpp:func:`select`
- :example:`peripherals/uart/uart_select`
- :example:`system/select`
如果 :cpp:func:`select` 用于套接字文件描述符,您可以启用 :envvar:`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``。各个文件系统驱动则可能会对自己的文件名长度设置一些限制。
文件描述符
----------------
文件描述符是一组很小的正整数,从 ``0````FD_SETSIZE - 1````FD_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``,则 ``stdin````stdout````stderr`` 将默认从 UART 读取或写入。UART0 或 UART1 可用作标准 IO。默认情况下UART0 使用 115200 波特率TX 管脚为 GPIO1RX 管脚为 GPIO3。您可以在 menuconfig 中更改上述参数。
``stdout````stderr`` 执行写入操作将会向 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_port_set_rx_line_endings````esp_vfs_dev_uart_port_set_tx_line_endings`` 为输入输出配置换行符。
标准流和 FreeRTOS 任务
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``stdin````stdout````stderr````FILE`` 对象在所有 FreeRTOS 任务之间共享,指向这些对象的指针分别存储在每个任务的 ``struct _reent`` 中。
预处理器把如下代码:
.. highlight:: c
::
fprintf(stderr, "42\n");
解释为:
.. highlight:: c
::
fprintf(__getreent()->_stderr, "42\n");
其中 ``__getreent()`` 函数将为每个任务返回一个指向 ``struct _reent`` 的指针。每个任务的 TCB 均拥有一个 ``struct _reent`` 结构体,任务初始化后,``struct _reent`` 结构体中的 ``_stdin````_stdout````_stderr`` 将会被赋予 ``_GLOBAL_REENT````_stdin````_stdout````_stderr`` 的值,``_GLOBAL_REENT`` 即为 FreeRTOS 启动之前所用结构体。
这样设计带来的结果是:
- 允许重定向给定任务的 ``stdin````stdout````stderr``,而不影响其他任务,例如通过 ``stdin = fopen("/dev/uart/1", "r")``
- 但使用 ``fclose`` 关闭默认 ``stdin````stdout````stderr`` 将同时关闭相应的 ``FILE`` 流对象,因此会影响其他任务;
- 如需更改新任务的默认 ``stdin````stdout````stderr`` 流,请在创建新任务之前修改 ``_GLOBAL_REENT->_stdin`` (``_stdout````_stderr``)。

View File

@ -404,6 +404,16 @@ FreeModBus
If using Modbus, enable or disable :ref:`CONFIG_FMB_COMM_MODE_TCP_EN`, :ref:`CONFIG_FMB_COMM_MODE_RTU_EN`, :ref:`CONFIG_FMB_COMM_MODE_ASCII_EN` as applicable for the necessary functionality.
VFS
@@@
:doc:`Virtual filesystem </api-reference/storage/vfs>` feature in ESP-IDF allows multiple filesystem drivers and file-like peripheral drivers to be accessed using standard I/O functions (``open``, ``read``, ``write``, etc.) and C library functions (``fopen``, ``fread``, ``fwrite``, etc.). When filesystem or file-like peripheral driver functionality is not used in the application this feature can be fully or partially disabled. VFS component provides the following configuration options:
* :ref:`CONFIG_VFS_SUPPORT_TERMIOS` — can be disabled if the application doesn't use ``termios`` family of functions. Currently, these functions are implemented only for UART VFS driver. Most applications can disable this option. Disabling this option reduces the code size by about 1.8 kB.
* :ref:`CONFIG_VFS_SUPPORT_SELECT` can be disabled if the application doesn't use ``select`` function with file descriptors. Currently, only the UART and eventfd VFS drivers implement ``select`` support. Note that when this option is disabled, ``select`` can still be used for socket file descriptors. Disabling this option reduces the code size by about 2.7 kB.
* :ref:`CONFIG_VFS_SUPPORT_DIR` — can be disabled if the application doesn't use directory related functions, such as ``readdir`` (see the description of this option for the complete list). Applications which only open, read and write specific files and don't need to enumerate or create directories can disable this option, reducing the code size by 0.5 kB or more, depending on the filesystem drivers in use.
* :ref:`CONFIG_VFS_SUPPORT_IO` — can be disabled if the application doesn't use filesystems or file-like peripheral drivers. This disables all VFS functionality, including the three options mentioned above. When this option is disabled, :doc:`console </api-reference/system/console>` can't be used. Note that the application can still use standard I/O functions with socket file descriptors when this option is disabled. Compared to the default configuration, disabling this option reduces code size by about 9.4 kB.
Bootloader Size
---------------

View File

@ -1,4 +1,252 @@
.. include:: ../../../../components/vfs/README.rst
Virtual filesystem component
============================
:link_to_translation:`zh_CN:[中文]`
Overview
--------
Virtual filesystem (VFS) component provides a unified interface for drivers which can perform operations on file-like objects. These can be real filesystems (FAT, SPIFFS, etc.) or device drivers which provide a file-like interface.
This component allows C library functions, such as fopen and fprintf, to work with FS drivers. At a high level, each FS driver is associated with some path prefix. When one of C library functions needs to open a file, the VFS component searches for the FS driver associated with the file 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.
For example, one can register a FAT filesystem driver with the ``/fat`` prefix and call ``fopen("/fat/file.txt", "w")``. The VFS component will then call the function ``open`` of the FAT driver and pass the argument ``/file.txt`` to it together with appropriate mode flags. All subsequent calls to C library functions for the returned ``FILE*`` stream will also be forwarded to the FAT driver.
FS registration
---------------
To register an FS driver, an application needs to define an instance of the :cpp:type:`esp_vfs_t` structure and populate it with function pointers to FS APIs:
.. highlight:: c
::
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 how the FS driver declares its API functions, either ``read``, ``write``, etc., or ``read_p``, ``write_p``, etc., should be used.
Case 1: API functions are declared without an extra context pointer (the FS driver is a singleton)::
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));
Case 2: API functions are declared with an extra context pointer (the FS driver supports multiple instances)::
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));
Synchronous input/output multiplexing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Synchronous input/output multiplexing by :cpp:func:`select` is supported in the VFS component. The implementation
works in the following way.
1. :cpp:func:`select` is called with file descriptors which could belong to various VFS drivers.
2. The file descriptors are divided into groups each belonging to one VFS driver.
3. The file descriptors belonging to non-socket VFS drivers are handed over to the given VFS drivers by :cpp:func:`start_select`
described later on this page. This function represents the driver-specific implementation of :cpp:func:`select` for
the given driver. This should be a non-blocking call which means the function should immediately return after setting up
the environment for checking events related to the given file descriptors.
4. The file descriptors belonging to the socket VFS driver are handed over to the socket driver by
:cpp:func:`socket_select` described later on this page. This is a blocking call which means that it will return only
if there is an event related to socket file descriptors or a non-socket driver signals :cpp:func:`socket_select`
to exit.
5. Results are collected from each VFS driver and all drivers are stopped by deinitiazation
of the environment for checking events.
6. The :cpp:func:`select` call ends and returns the appropriate results.
Non-socket VFS drivers
""""""""""""""""""""""
If you want to use :cpp:func:`select` with a file descriptor belonging to a non-socket VFS driver
then you need to register the driver with functions :cpp:func:`start_select` and
:cpp:func:`end_select` similarly to the following example:
.. highlight:: c
::
// In definition of esp_vfs_t:
.start_select = &uart_start_select,
.end_select = &uart_end_select,
// ... other members initialized
:cpp:func:`start_select` is called for setting up the environment for
detection of read/write/error conditions on file descriptors belonging to the
given VFS driver.
:cpp:func:`end_select` is called to stop/deinitialize/free the
environment which was setup by :cpp:func:`start_select`.
.. note::
:cpp:func:`end_select` might be called without a previous :cpp:func:`start_select` call in some rare
circumstances. :cpp:func:`end_select` should fail gracefully if this is the case.
Please refer to the
reference implementation for the UART peripheral in
:component_file:`vfs/vfs_uart.c` and most particularly to the functions
:cpp:func:`esp_vfs_dev_uart_register`, :cpp:func:`uart_start_select`, and
:cpp:func:`uart_end_select` for more information.
Please check the following examples that demonstrate the use of :cpp:func:`select` with VFS file descriptors:
- :example:`peripherals/uart/uart_select`
- :example:`system/select`
Socket VFS drivers
""""""""""""""""""
A socket VFS driver is using its own internal implementation of :cpp:func:`select` and non-socket VFS drivers notify
it upon read/write/error conditions.
A socket VFS driver needs to be registered with the following functions defined:
.. highlight:: c
::
// In definition of esp_vfs_t:
.socket_select = &lwip_select,
.get_socket_select_semaphore = &lwip_get_socket_select_semaphore,
.stop_socket_select = &lwip_stop_socket_select,
.stop_socket_select_isr = &lwip_stop_socket_select_isr,
// ... other members initialized
:cpp:func:`socket_select` is the internal implementation of :cpp:func:`select` for the socket driver. It works only
with file descriptors belonging to the socket VFS.
:cpp:func:`get_socket_select_semaphore` returns the signalization object (semaphore) which will be used in non-socket
drivers to stop the waiting in :cpp:func:`socket_select`.
:cpp:func:`stop_socket_select` call is used to stop the waiting in :cpp:func:`socket_select` by passing the object
returned by :cpp:func:`get_socket_select_semaphore`.
:cpp:func:`stop_socket_select_isr` has the same functionality as :cpp:func:`stop_socket_select` but it can be used
from ISR.
Please see :component_file:`lwip/port/esp32/vfs_lwip.c` for a reference socket driver implementation using LWIP.
.. note::
If you use :cpp:func:`select` for socket file descriptors only then you can enable the
:envvar:`CONFIG_LWIP_USE_ONLY_LWIP_SELECT` option to reduce the code size and improve performance.
.. note::
Don't change the socket driver during an active :cpp:func:`select` call or you might experience some undefined
behavior.
Paths
-----
Each registered FS has a path prefix associated with it. This prefix can be considered as a "mount point" of this partition.
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:
- FS 1 on /data
- FS 2 on /data/static
Then:
- 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"`` does not exist in FS 2, FS 1 will *not* be searched for ``/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 might be used in cases when an application needs to provide a "fallback" filesystem or to override VFS functionality altogether. Such filesystem will be used if no prefix matches the path given.
VFS does not handle dots (``.``) in path names in any special way. VFS does not treat ``..`` as a reference to the parent directory. 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) might handle dots in file names differently.
When opening files, the FS driver receives only relative paths to files. For example:
1. The ``myfs`` driver is registered with ``/data`` as a path prefix.
2. The application calls ``fopen("/data/config.json", ...)``.
3. The VFS component calls ``myfs_open("/config.json", ...)``.
4. The ``myfs`` driver opens the ``/config.json`` file.
VFS does not impose any limit on total file path length, but it does limit the FS path prefix to ``ESP_VFS_PATH_MAX`` characters. Individual FS drivers may have their own filename length limitations.
File descriptors
----------------
File descriptors are small positive integers from ``0`` to ``FD_SETSIZE - 1``, where ``FD_SETSIZE`` is defined in newlib's ``sys/types.h``. The largest file descriptors (configured by ``CONFIG_LWIP_MAX_SOCKETS``) are reserved for sockets. The VFS component contains a lookup-table called ``s_fd_table`` for mapping global file descriptors to VFS driver indexes registered in the ``s_vfs`` array.
Standard IO streams (stdin, stdout, stderr)
-------------------------------------------
If the menuconfig option ``UART for console output`` 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; RX pin is GPIO3. These parameters can be changed in 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.
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. Due to this non-blocking read behavior, higher level C library calls, such as ``fscanf("%d\n", &var);``, might not have desired results.
Applications which use the UART driver can instruct VFS to use the driver's interrupt driven, blocking read and write functions instead. This can be done using a call to the ``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``.
VFS also provides an optional newline conversion feature for input and output. Internally, most applications send and receive lines terminated by the 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 the functions ``esp_vfs_dev_uart_port_set_rx_line_endings`` and ``esp_vfs_dev_uart_port_set_tx_line_endings``.
Standard streams and FreeRTOS tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``FILE`` objects for ``stdin``, ``stdout``, and ``stderr`` are shared between all FreeRTOS tasks, but the pointers to these objects are stored in per-task ``struct _reent``.
The following code is transferred to ``fprintf(__getreent()->_stderr, "42\n");`` by the preprocessor:
.. highlight:: c
::
fprintf(stderr, "42\n");
The ``__getreent()`` function returns a per-task pointer to ``struct _reent`` in newlib libc. 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).
Such a design has the following consequences:
- It is possible to set ``stdin``, ``stdout``, and ``stderr`` for any given task without affecting other tasks, e.g., by doing ``stdin = fopen("/dev/uart/1", "r")``.
- Closing default ``stdin``, ``stdout``, or ``stderr`` using ``fclose`` will close the ``FILE`` stream object, which 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.
Event fds
-------------------------------------------
``eventfd()`` call is a powerful tool to notify a ``select()`` based loop of custom events. The ``eventfd()`` implementation in ESP-IDF is generally the same as described in ``man(2) eventfd`` except for:
- ``esp_vfs_eventfd_register()`` has to be called before calling ``eventfd()``
- Options ``EFD_CLOEXEC``, ``EFD_NONBLOCK`` and ``EFD_SEMAPHORE`` are not supported in flags.
- Option ``EFD_SUPPORT_ISR`` has been added in flags. This flag is required to read and the write the eventfd in an interrupt handler.
Note that creating an eventfd with ``EFD_SUPPORT_ISR`` will cause interrupts to be temporarily disabled when reading, writing the file and during the beginning and the ending of the ``select()`` when this file is set.
API Reference
-------------

View File

@ -1,4 +1,171 @@
.. include:: ../../../../components/vfs/README_CN.rst
虚拟文件系统组件
============================
:link_to_translation:`en:[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 驱动程序,首先要定义一个 :cpp:type:`esp_vfs_t` 结构体实例,并用指向 FS API 的函数指针填充它。
.. highlight:: c
::
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));
在上述代码中需要用到 ``read````write````read_p````write_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));
同步输入/输出多路复用
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
如需通过 :cpp:func:`select` 使用同步输入/输出多路复用,首先需要把 :cpp:func:`start_select`:cpp:func:`end_select` 注册到 VFS如下所示
.. highlight:: c
::
// In definition of esp_vfs_t:
.start_select = &uart_start_select,
.end_select = &uart_end_select,
// ... other members initialized
调用 :cpp:func:`start_select` 设置环境,用以检测某一 VFS 文件描述符的读取/写入/错误条件。调用 :cpp:func:`end_select` 终止、析构或释放 :cpp:func:`start_select` 设置的资源。请在 :component_file:`vfs/vfs_uart.c` 中查看 UART 外设参考实现、:cpp:func:`esp_vfs_dev_uart_register`:cpp:func:`uart_start_select`:cpp:func:`uart_end_select` 函数。
请参考以下示例,查看如何使用 VFS 文件描述符调用 :cpp:func:`select`
- :example:`peripherals/uart/uart_select`
- :example:`system/select`
如果 :cpp:func:`select` 用于套接字文件描述符,您可以启用 :envvar:`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``。各个文件系统驱动则可能会对自己的文件名长度设置一些限制。
文件描述符
----------------
文件描述符是一组很小的正整数,从 ``0````FD_SETSIZE - 1````FD_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``,则 ``stdin````stdout````stderr`` 将默认从 UART 读取或写入。UART0 或 UART1 可用作标准 IO。默认情况下UART0 使用 115200 波特率TX 管脚为 GPIO1RX 管脚为 GPIO3。您可以在 menuconfig 中更改上述参数。
``stdout````stderr`` 执行写入操作将会向 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_port_set_rx_line_endings````esp_vfs_dev_uart_port_set_tx_line_endings`` 为输入输出配置换行符。
标准流和 FreeRTOS 任务
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``stdin````stdout````stderr````FILE`` 对象在所有 FreeRTOS 任务之间共享,指向这些对象的指针分别存储在每个任务的 ``struct _reent`` 中。
预处理器把如下代码:
.. highlight:: c
::
fprintf(stderr, "42\n");
解释为:
.. highlight:: c
::
fprintf(__getreent()->_stderr, "42\n");
其中 ``__getreent()`` 函数将为每个任务返回一个指向 ``struct _reent`` 的指针。每个任务的 TCB 均拥有一个 ``struct _reent`` 结构体,任务初始化后,``struct _reent`` 结构体中的 ``_stdin````_stdout````_stderr`` 将会被赋予 ``_GLOBAL_REENT````_stdin````_stdout````_stderr`` 的值,``_GLOBAL_REENT`` 即为 FreeRTOS 启动之前所用结构体。
这样设计带来的结果是:
- 允许重定向给定任务的 ``stdin````stdout````stderr``,而不影响其他任务,例如通过 ``stdin = fopen("/dev/uart/1", "r")``
- 但使用 ``fclose`` 关闭默认 ``stdin````stdout````stderr`` 将同时关闭相应的 ``FILE`` 流对象,因此会影响其他任务;
- 如需更改新任务的默认 ``stdin````stdout````stderr`` 流,请在创建新任务之前修改 ``_GLOBAL_REENT->_stdin`` (``_stdout````_stderr``)。
应用示例
-------------------