Zephyr的文件描述符管理模块fdtable

Creative Commons
本作品采用知识共享署名

本文说明Zephyr的fdtable和posix标准输出的实现。

和Linux相似,Zephyr也有自己的fdtable用来管理文件描述符,相对的Zephyr的fdtable实现很简单。所有可以抽象为read/write/close/ioctl的模块都可以注册到fdtable中,通过对fd的read/write/close/ioctl统一形式API来操作,目前Zephyr的fdtable主要用在posix文件系统和socket的fd管理。本文主要分析fdtable的实现以及起在posix下标准输入输出上的使用。

API说明

include/sys/fdtable.h中定义了对外API和数据结构

数据结构

文件描述符的虚方法表,提供了标准抽象:读,写,关闭,其它操作都是通过ioctl来完成

1
2
3
4
5
6
struct fd_op_vtable {
ssize_t (*read)(void *obj, void *buf, size_t sz);
ssize_t (*write)(void *obj, const void *buf, size_t sz);
int (*close)(void *obj);
int (*ioctl)(void *obj, unsigned int request, va_list args);
};

API

int z_reserve_fd(void)
预定文件描述符,返回值就是int型的描述符

void z_finalize_fd(int fd, void *obj, const struct fd_op_vtable *vtable)
作用创建文件描述符,在z_reserve_fd后只调用一次,将I/O的对象obj和操作方法表vtable注册给文件描述符

int z_alloc_fd(void *obj, const struct fd_op_vtable *vtable)
分配文件描述符,其作用就是z_reserve_fd和z_finalize_fd的组合

void z_free_fd(int fd)
释放描述符

void *z_get_fd_obj(int fd, const struct fd_op_vtable *vtable, int err)
获取文件描述符操作的IO对象,返回的就是z_finalize_fd注册的obj,如果vtable不为NULL,将匹配vtable和注册时一致才会返回obj

void *z_get_fd_obj_and_vtable(int fd, const struct fd_op_vtable **vtable)
获取文件描述符操作的IO兑现,返回的是z_finalize_fd注册的obj,输出的vtable是z_finalize_fd注册的vtable

static inline int z_fdtable_call_ioctl(const struct fd_op_vtable *vtable, void *obj,unsigned long request, ...)
调用vtable中的ioctrl,request和posix函数fcntl的cmd参数定义一致,zephyr定义了0x100以上的几个私有的request,如下:

1
2
3
4
5
6
7
8
enum {
/* Codes below 0x100 are reserved for fcntl() codes. */
ZFD_IOCTL_FSYNC = 0x100,
ZFD_IOCTL_LSEEK,
ZFD_IOCTL_POLL_PREPARE,
ZFD_IOCTL_POLL_UPDATE,
ZFD_IOCTL_POLL_OFFLOAD,
};

实现

fdtable实现代码在lib/os/fdtable.c中,实现方法比较简单。在其内部维护一个struct fd_entry的数组fdtable

1
2
3
4
5
struct fd_entry {
void *obj; //IO object
const struct fd_op_vtable *vtable; //文件描述符操作方法表
atomic_t refcount; //文件描述符引用次数
};

fdtable的大小由CONFIG_POSIX_MAX_FDS配置
在预定fd时会顺序检查fdtable,取出一个refcount为0的成员,将其index序号做为fd返回,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int _find_fd_entry(void)
{
int fd;
//顺序检查
for (fd = 0; fd < ARRAY_SIZE(fdtable); fd++) {
//找出refcount为0的成员,返回其在fdtable的index作为fd
if (!atomic_get(&fdtable[fd].refcount)) {
return fd;
}
}

errno = ENFILE;
return -1;
}

int z_reserve_fd(void)
{
int fd;

(void)k_mutex_lock(&fdtable_lock, K_FOREVER);

//找到没有使用的fd
fd = _find_fd_entry();
if (fd >= 0) {
//增加refcount引用,表示该fd已用
(void)z_fd_ref(fd);
//obj和vtable将在z_finalize_fd中设置
fdtable[fd].obj = NULL;
fdtable[fd].vtable = NULL;
}

k_mutex_unlock(&fdtable_lock);

return fd;
}

z_finalize_fd实现很简单,就是将obj和vtable设置到fdtable内

1
2
3
4
5
void z_finalize_fd(int fd, void *obj, const struct fd_op_vtable *vtable)
{
fdtable[fd].obj = obj;
fdtable[fd].vtable = vtable;
}

z_alloc_fd就是z_reserve_fd和z_finalize_fd的组合,这里不再分析代码
z_get_fd_obj和z_get_fd_obj_and_vtable也是通过fd查表fdtable,取得obj和vtable,代码简单这里不做分析。
z_free_fd是释放掉占用的fdtable

1
2
3
4
5
void z_free_fd(int fd)
{
//对refcount进行减一操作
(void)z_fd_unref(fd);
}

可以从下面的code看到z_fd_ref是对refcount加一,z_fd_unref对refcount减一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int z_fd_ref(int fd)
{
return atomic_inc(&fdtable[fd].refcount) + 1;
}

static int z_fd_unref(int fd)
{
atomic_val_t old_rc;
//refcount减一操作
do {
old_rc = atomic_get(&fdtable[fd].refcount);
if (!old_rc) {
return 0;
}
} while (!atomic_cas(&fdtable[fd].refcount, old_rc, old_rc - 1));

//如果还有其它引用就返回
if (old_rc != 1) {
return old_rc - 1;
}

//没有引用就清空obj和vtable
fdtable[fd].obj = NULL;
fdtable[fd].vtable = NULL;

return 0;
}

虽然在实现fdtable的管理里面使用了ref机制,但目前fdtable ref只会在z_reserve_fd被增加一次,因此z_free_fd调用z_fd_unref就会被释放。

POSIX API

当zephyr配置了CONFIG_POSIX_API=y时,会使用fdtable中实现的标准IO函数,并实现标准输入输出。

标准IO

标准IO上实现了,read/write/close/fsync/lseek/ioctl/fcntl, libc的桩函数也会直接链接到这些里面来
这些标准IO实现上就是直接调用对应fd 的vtable内函数,例如read,就是调用fd对应fdtable[fd].vtable->read,其它的就不再列出可以到fdtable中查看

1
2
3
4
5
6
7
8
ssize_t read(int fd, void *buf, size_t sz)
{
if (_check_fd(fd) < 0) {
return -1;
}

return fdtable[fd].vtable->read(fdtable[fd].obj, buf, sz);
}

标准输入输出

在配置CONFIG_POSIX_API=y后, fdtable默认注册了标准输入输出的fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static struct fd_entry fdtable[CONFIG_POSIX_MAX_FDS] = {
#ifdef CONFIG_POSIX_API
/*
* Predefine entries for stdin/stdout/stderr.
*/
{
/* STDIN */
.vtable = &stdinout_fd_op_vtable,
.refcount = ATOMIC_INIT(1)
},
{
/* STDOUT */
.vtable = &stdinout_fd_op_vtable,
.refcount = ATOMIC_INIT(1)
},
{
/* STDERR */
.vtable = &stdinout_fd_op_vtable,
.refcount = ATOMIC_INIT(1)
},
#endif
};

可以看到fd=0是标准输入,fd=1是标准输出,fd=2是标准error,三者都使用了stdinout_fd_op_vtable

1
2
3
4
5
static const struct fd_op_vtable stdinout_fd_op_vtable = {
.read = stdinout_read_vmeth,
.write = stdinout_write_vmeth,
.ioctl = stdinout_ioctl_vmeth,
};

从标准输入中读将调用到stdinout_read_vmeth, 向标准输出写会调用到stdinout_write_vmeth, 对标准输入输出进行控制会调用到stdinout_ioctl_vmeth,在目前的实现中没有支持标准输入和控制,因此代码中stdinout_read_vmeth/stdinout_ioctl_vmeth是留的空函数,这里分析标准输出函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ssize_t stdinout_write_vmeth(void *obj, const void *buffer, size_t count)
{
return z_impl_zephyr_write_stdout(buffer, count);
#endif
}

int z_impl_zephyr_write_stdout(const void *buffer, int nbytes)
{
const char *buf = buffer;
int i;

for (i = 0; i < nbytes; i++) {
if (*(buf + i) == '\n') {
_stdout_hook('\r');
}
_stdout_hook(*(buf + i));
}
return nbytes;
}

标准输出也就是对fd为1进行写,就会调用到z_impl_zephyr_write_stdout,该操作会自动将\n变为\r\b,然后调用_stdout_hook进行输出。
_stdout_hook由__stdout_hook_install进行安装,一般情况下是安装到串口,可以用串口中断产看

1
Uart_console.c (drivers\console):	__stdout_hook_install(console_out);

也可以根据需要安装到其它地方,例如下面两种是向调试器写,可以在host上用工具查看

1
2
Semihost_console.c (drivers\console):	__stdout_hook_install(semihost_console_out);
Rtt_console.c (drivers\console): __stdout_hook_install(rtt_console_out);

下面这一种是写到ram中,也是为了调试用

1
Ram_console.c (drivers\console):	__stdout_hook_install(ram_console_out);

下面这种是写到蓝牙monitor

1
Monitor.c (subsys\bluetooth\host):	__stdout_hook_install(monitor_console_out);

文件系统

fdtable中提供了标准的read/write/close/ioctl,哪open哪里去了呢,其实open就是从fdtable中获取描述符,然后将自己的read/write/close/ioctl注册进去,lib/posix/fs.c中open的流程可以看到这一点,下面是简化后的code

1
2
3
4
5
6
7
8
9
10
11
12
13
int open(const char *name, int flags, ...)
{
fd = z_reserve_fd();

ptr = posix_fs_alloc_obj(false);

rc = fs_open(&ptr->file, name, zmode);

z_finalize_fd(fd, ptr, &fs_fd_op_vtable);

return fd;

}

可以看到从fdtable申请到fd,将正在文件系统的操作函数注册到vtable内

1
2
3
4
5
6
static struct fd_op_vtable fs_fd_op_vtable = {
.read = fs_read_vmeth,
.write = fs_write_vmeth,
.close = fs_close_vmeth,
.ioctl = fs_ioctl_vmeth,
};

实际执行read的时候就是执行的fs_read_vmeth, 最终还是操作的fs_read函数。 write/close/ioctl类似,不再做展开分析

1
2
3
4
5
6
7
8
9
10
11
12
13
static ssize_t fs_read_vmeth(void *obj, void *buffer, size_t count)
{
ssize_t rc;
struct posix_fs_desc *ptr = obj;

rc = fs_read(&ptr->file, buffer, count);
if (rc < 0) {
errno = -rc;
return -1;
}

return rc;
}

关于socket

socket和文件系统使用fdtable的方式是一致的,只是socket除了标准的read/write/ioctl还有其它API,在之后的offload socket会详细分析,这里就不展开了。