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

本文说明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来完成

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,如下:

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

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返回,代码如下

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内

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

void z_free_fd(int fd)
{
	//对refcount进行减一操作
	(void)z_fd_unref(fd);
}

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

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中查看

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

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

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是留的空函数,这里分析标准输出函数:

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进行安装,一般情况下是安装到串口,可以用串口中断产看

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

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

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

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

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

下面这种是写到蓝牙monitor

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

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内

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类似,不再做展开分析

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会详细分析,这里就不展开了。