虚拟设备文件
Linux 内核自 2.6.22 版本开始逐步增加了三个虚拟设备文件:eventfd、timerfd、signalfd。这三个文件让应用程序可以通过标准 I/O 操作的方式代替传统调用 API 的方式来使用事件(信号量)、定时器和信号资源,这带来的最大好处是应用程序可以通过使用 select(或 poll、epoll)同时监听多个此类文件(或此类文件与其他文件),将对多个事件的异步并行处理方便地转化为同步串行处理,这在许多应用中非常有用。SylixOS 完全兼容这三个虚拟设备文件,并且增加了一个 hstimerfd,用于高精度定时器。
下面将分别介绍它们的使用方法。
eventfd
eventfd 主要用于线程之间的事件通知,Linux 由于支持 fork 系统调用,因此也可以用于父子进程之间的事件通知。相关 API 介绍如下:
int eventfd(unsigned int initval, int flags);
int eventfd_read(int fd, eventfd_t *value);
int eventfd_write(int fd, eventfd_t value);
函数 eventfd 原型分析:
- 该函数成功返回一个文件描述符,失败返回负数并设置错误号。
- 参数 initval 表示事件的初始状态,例如为 0 表示当前没有任何事件产生。
- 参数 flags 为该事件文件的操作选项,可以为 EFD_CLOEXEC、EFD_NONBLOCK、和 EFD_SEMAPHORE。EFD_SEMAPHORE 的具体含义随后讲解。
函数 eventfd_read 用于读取事件文件,即等待事件的发送。当没有事件且未使用 EFD_NONBLOCK 参数,该函数将会阻塞,其原型分析如下:
- 该函数成功返回 0,失败返回错误码。
- 参数 fd 即为通过 eventfd 打开的一个事件文件描述符。
- 输出参数 value 保存读取的事件数量值,其行为与 EFD_SEMAPORE 标志有关。eventfd_t 在大多数系统中被定义为一个无符号 64 位整型数,正常情况下,它能表示的事件数量可以理解为无限多。
当使用 eventfd 函数的参数 flags 包含了 EFD_SEMAPHORE 时,其含义为将事件按照计数信号量来操作。即如果当前已经产生了多个事件,每一次读取只会获得一个事件,该文件内部的事件计数器只减一, value 里面的内容为 1。当没有使用 EFD_SEMAPHORE 时,一次读取所有的事件,value 里面的内容即为读取的事件数量,内部的事件计数器归零。两种方式可满足应用程序在不同场景的需求。
函数 eventfd_write 用于写事件文件,即发送事件。当内部事件计数器达到最大值时,将不能继续发送事件,此时返回错误,其原型分析如下:
- 该函数成功返回 0,失败返回错误码。
- 参数 fd 即为通过 eventfd 打开的一个事件文件描述符。
- 输入参数 value 为发送的事件数量,其行为与 EFD_SEMAPHORE 标志有关。
下面的例子展示了 eventfd 的使用方法。
#include <stdio.h>
#include <sys/eventfd.h>
#include <pthread.h>
void *event_write_routine (void *arg)
{
eventfd_t value = 1;
int event_fd = (int)arg;
int ret;
while (1) {
ret = eventfd_write(event_fd, value);
if (ret) {
fprintf(stderr, "write eventfd error.\n");
break;
}
value++;
}
return (NULL);
}
int event_write_server_start (int event_fd, void *(*routine)(void *))
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, routine, (void *)event_fd);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
return (0);
}
int main (int argc, char *arvg[])
{
int event_fd;
int ret;
eventfd_t value;
event_fd = eventfd(0, 0);
if (event_fd < 0) {
fprintf(stderr, "open eventfd failed.\n");
return (-1);
}
ret = event_write_server_start(event_fd, event_write_routine);
if (ret) {
fprintf(stderr, "start eventfd write server failed.\n");
close(event_fd);
return (-1);
}
while (1) {
ret = eventfd_read(event_fd, &value);
if (ret) {
fprintf(stderr, "read eventfd error.\n");
break;
}
fprintf(stdout, "read event value count: %llu.\n", value);
}
close(event_fd);
return (0);
}
在上面的程序中,子线程不断地往事件文件写入数值递增的事件,主线程不断地读取事件并打印本次事件的数值。其运行结果如下:
# ./event_Synchronization_between_threads
read event value count: 1.
read event value count: 2.
read event value count: 3.
read event value count: 4.
read event value count: 5.
read event value count: 6.
read event value count: 7.
read event value count: 8.
上面的结果非常规律,可以看出,每一次读取的事件数值反映的仅仅是一次写入的事件数值,而不会是多次写入数值的总和。实际上,eventfd_write 函数总是会等待上一次写入的事件被读取后才能完成本次事件的写入,所以,事件文件的读和写是一个相互同步的过程,一次事件的数值可理解为事件发生后所能处理的资源数量,EFD_SEMAPHORE 则允许事件接收方决定是一次处理多个资源还是单个资源。
timerfd
在“时间管理”中,我们曾介绍了使用 SylixOS 定时器相关的 API 函数,使用 timerfd 也同样能够实现定时器功能,相关 API 如下。
#include <timerfd.h>
int timerfd_create(clockid_t clockid, int flags);
int timerfd_settime(int fd, int flags, const struct itimerspec *ntmr,
struct itimerspec *otmr);
int timerfd_gettime(int fd, struct itimerspec *currvalue);
函数 timerfd_create 原型分析:
- 该函数成功返回定时器文件描述符,失败返回负数并设置错误号。
- 参数 clockid 表示该定时器参考的时钟源类型,可以为 CLOCK_REALTIME 或 CLOCK_MONOTONIC,分别代表真实时间和线性递增时间。
- 参数 flags 是定时器文件选项位标识,可以是 TFD_CLOEXEC(等于 O_CLOEXEC)和 TFD_NONBLOCK(等于 O_NONBLOCK)。
函数 timerfd_settime 用于设置定时器启动时间以及重载时间间隔。原型分析如下:
- 该函数成功返回 0,失败返回负数并设置错误号。
- 参数 fd 为定时器文件描述符。
- 参数 flags 为与定时时间相关的标识,可以是 0 或者 TIMER_ABSTIME,它影响参数 ntmr 的含义。
- 参数 ntmr 描述了定时器的时间参数:启动时间和重载时间。
- 输出参数 otmr 保存旧的时间参数。可以为 NULL。
数据类型 itimerspec 定义如下:
struct itimerspec {
struct timespec it_interval; /* 定时器重载值 */
struct timespec it_value; /* 到下一次到期为止剩余时间 */
};
it_interval 为定时器周期触发的时间间隔,it_value 表示定时器第一次触发的时间。该值的含义由 flags 定义,如果设置了 TIMER_ABSTIME 标识,则表示 it_value 为一个绝对时间,当系统时间到达该值时定时器触发。如果 flags 为 0,则表示 it_value 为一个相对时间,经过该时间值后定时器触发。两者的类型都为 timespec,这意味着,定时器可以达到纳秒级别的时间精度。
函数 timerfd_gettime 获得定时器文件当前的时间参数,其原型分析如下:
- 该函数成功返回 0,失败返回负数并设置错误号。
- 参数 fd 为定时器文件描述符。
- 输出参数 currvalue 保存当前时间参数。
一旦调用 timerfd_settime 成功,即会启动定时器,并在满足设置的时间条件时触发定时器。应用程序可使用 read 或 select 来等待定时器触发(与 GPIO 设备相同)。
程序清单 timerfd应用示例(Application_of_Timerfd)
#include <sys/timerfd.h>
#include <stdio.h>
#include <stdint.h>
#include <time.h>
static int show_elapsed_time (void)
{
static struct timespec start;
struct timespec curr;
static int first_call = 1;
int secs;
int nsecs;
if (first_call) {
first_call = 0;
if (clock_gettime(CLOCK_MONOTONIC, &start) == -1) {
return (-1);
}
}
if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1) {
return (-1);
}
secs = curr.tv_sec - start.tv_sec;
nsecs = curr.tv_nsec - start.tv_nsec;
if (nsecs < 0) {
secs--;
nsecs += 1000000000;
}
fprintf(stdout, "time elapsed: %d.%03d seconds.\n",
secs, (nsecs + 500000) / 1000000);
return (0);
}
int main(int argc, char *argv[])
{
struct itimerspec time;
int timer_fd;
int ret;
uint64_t expired;
ssize_t read_len;
timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);
if (timer_fd < 0) {
fprintf(stderr, "create timerfd failed.\n");
return (-1);
}
time.it_value.tv_sec = 3;
time.it_value.tv_nsec = 0;
time.it_interval.tv_sec = 1;
time.it_interval.tv_nsec = 0;
ret = timerfd_settime(timer_fd, 0, &time, NULL);
if (ret) {
fprintf(stderr, "start timerfd error.\n");
close(timer_fd);
return (-1);
}
ret = show_elapsed_time();
if (ret) {
close(timer_fd);
return (-1);
}
while (1) {
read_len = read(timer_fd, &expired, sizeof(uint64_t));
if (read_len < sizeof(uint64_t)) {
fprintf(stderr, "read timerfd error.\n");
break;
}
ret = show_elapsed_time();
if (ret) {
break;
}
fprintf(stdout, "timer is triggered, expire count = %llu.\n", expired);
sleep(2);
}
close(timer_fd);
return (0);
}
上面的程序中,创建定时器时,其参数 flags 为 0,表示使用相对时间,在调用 timerfd_settime 时,其时间参数的设置表示,定时器自启动后经过 3 秒触发,之后每隔 1 秒触发一次。私有函数 show_elapsed_time 用于显示自定时器启动后所经过的时间,以此作为检验定时器精确度的一个标准。程序在等待定时器第一次触发后,每隔 2 秒周期性地去等待定时器的触发状态。
注意,这里通过 read 的方式等待定时器触发,其读取的数据为一个 64 位无符号类型,该数据表示的是定时器到本次触发为止总共触发了多少次,我们也把它叫做定时器到期的次数。
程序运行后,结果如下:
# ./Application_of_Timerfd
time elapsed: 0.000 seconds.
time elapsed: 2.996 seconds.
timer is triggered, expire count = 1.
time elapsed: 4.996 seconds.
timer is triggered, expire count = 2.
time elapsed: 6.996 seconds.
timer is triggered, expire count = 2.
time elapsed: 8.996 seconds.
timer is triggered, expire count = 2.
time elapsed: 10.996 seconds.
timer is triggered, expire count = 2.
time elapsed: 12.996 seconds.
timer is triggered, expire count = 2.
......
从结果可以看出,定时器在第一次触发时,经过了 2.996 秒(有一定精度偏差),并且显示定时器到期计数为 1,这符合我们的设定。在之后,由于定时器每隔 1 秒触发一次,因此我们每隔 2 秒去获得的定时器到期计数为 2,也与预期一致。
需要注意的是,设定定时器时间参数时,如果 it_value 的时间值为 0,并不表示定时器立即触发,而是表示停止定时器。同样的,it_interval 的时间值为 0,也不表示定时器无等待时间无限触发,而是停止定时器。
hstimerfd
SylixOS 支持时间精度可高于系统时钟的定时器,并提供相关 API 函数(见时间管理),高精度定时器只能保证其时间精度不低于普通定时器,这完全取决于系统硬件以及 BSP 包的支持。SylixOS 同样提供了类似于 timrfd 的 hstimerfd 文件,让应用程序通过标准 I/O 使用高精度定时器。相关 API 定义如下:
#include <sys/hstimerfd.h>
int hstimerfd_hz(void);
int hstimerfd_create(int flags);
int hstimerfd_settime(int fd,
const struct itimerspec *ntmr,
struct itimerspec *otmr);
int hstimerfd_settime2(int fd, hstimer_cnt_t *ncnt, hstimer_cnt_t *ocnt);
int hstimerfd_gettime(int fd, struct itimerspec *currvalue);
int hstimerfd_gettime2(int fd, hstimer_cnt_t *currvalue);
hstimerfd_settime 与 hstimerfd_gettime 与上一节的普通定时器 API 的参数和行为一致。hstimerfd_hz 函数返回高精度定时器的计数频率,亦是可以达到的定时精度。
函数 hstimerfd_create 原型分析:
- 该函数成功时返回文件描述符,失败时返回负数并设置错误号。
- 参数 flags 为文件标识,可以是 HSTFD_CLOEXEC(等于 O_CLOEXEC)和 HSTFD_NONBLOCK(等于 O_NONBLOCK)。
函数 hstimerfd_settime2 原型分析:
- 该函数成功返回 0,失败返回错误码。
- 参数 fd 为高精度定时器文件描述符。
- 参数 ncnt 类似为 hstimer_cnt_t,表示新的定时器时间计数参数。
- 输出参数 ocnt 保存旧的定时器时间计数参数。
hstimerfd 提供了新的时间参数方式,即使用高精度定时器的计数值来作为其时间参数,数据结构 hstimer_cnt_t 定义如下:
typedef struct hstimer_cnt {
unsigned long value;
unsigned long interval;
} hstimer_cnt_t;
成员变量 value 表示定时器自启动后第一次触发经过的计数值(即首次到期计数值),interval 为定时器周期触发的计数值。
函数 hstimerfd_gettime2 原型分析:
- 该函数成功返回 0,失败返回错误码。
- 参数 fd 为高精度定时器文件描述符。
- 输出参数 currvalue 保存定时器当前的时间参数。
高精度定时器除了时间精度与普通定时器有区别以外,其他行为与普通定时器相似,因此这里不再给出相关示例。读者可根据程序清单 imerfd应用示例(Application_of_Timerfd)的程序稍作修改,即可使用高精度定时器。
signalfd
传统的信号处理方式是使用 signal 或 sigaction 函数注册关心的信号处理函数,在信号发生时,这些函数会以异步的方式被调用,因此在使用中需要考虑数据并发的问题,其相关 API 及其使用方法见信号系统。signalfd 允许应用程序以文件的方式等待信号的产生并同步处理这些信号,signalfd 为一个标准 I/O 设备文件,其定义位于 <sys/signalfd.h> 头文件,如下所示:
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
函数 signalfd 原型分析:
- 该函数成功返回一个信号文件描述符,失败返回负数并设置错误号。
- 参数 fd 表示一个已有的信号文件描述符。如果为-1 则表示新创建一个信号文件;如果 fd 为一个已有的信号文件描述符,则表示重新设置其需要处理的信号。
- 参数 mask 为一个包含了需要关心的信号集。
- 参数 flags 可以为 SFD_CLOEXEC(等于 O_CLOEXEC)和 SFD_NONBLOCK(等于 O_NONBLOCK)。
当成功调用 signalfd 函数后,其返回的文件描述符即与参数 mask 所指定的信号进行了关联。之后使用 read 函数等待信号的发生,读取的数据为一个 signalfd_siginfo 结构,其定义如下:
struct signalfd_siginfo {
uint32_t ssi_signo; /* Signal number */
int32_t ssi_errno; /* Error number (unused) */
int32_t ssi_code; /* Signal code */
uint32_t ssi_pid; /* PID of sender */
uint32_t ssi_uid; /* Real UID of sender */
int32_t ssi_fd; /* File descriptor (SIGIO) */
uint32_t ssi_tid; /* Kernel timer ID (POSIX timers) */
uint32_t ssi_band; /* Band event (SIGIO) */
uint32_t ssi_overrun; /* POSIX timer overrun count */
uint32_t ssi_trapno; /* Trap number that caused signal */
int32_t ssi_status; /* Exit status or signal (SIGCHLD) */
int32_t ssi_int; /* Integer sent by sigqueue(3) */
uint64_t ssi_ptr; /* Pointer sent by sigqueue(3) */
uint64_t ssi_utime; /* User CPU time consumed (SIGCHLD) */
uint64_t ssi_stime; /* System CPU time consumed (SIGCHLD) */
uint64_t ssi_addr; /* Address that generated signal */
/* (for hardware-generated signals) */
uint8_t pad[48];
};
signalfd_siginfo 的成员变量 ssi_signo 为当前信号的编号,我们可以根据它对不同的信号作对应的处理。signalfd_siginfo 与 siginfo_t 的很多同名成员的含义相同,关于 siginfo_t 的详细介绍见信号系统。
下面以一个简单的例子展示 signalfd 的使用方法。
#include <sys/signalfd.h>
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
sigset_t sig_mask;
int sig_fd;
struct signalfd_siginfo sig_info;
ssize_t read_size;
sigemptyset(&sig_mask);
sigaddset(&sig_mask, SIGINT);
sigaddset(&sig_mask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &sig_mask, NULL) == -1) {
fprintf(stderr,"sigprocmask error.\n");
return (-1);
}
sig_fd = signalfd(-1, &sig_mask, 0);
if (sig_fd < 0) {
fprintf(stderr, "crteate signalfd error.\n");
return (-1);
}
while (1) {
read_size = read(sig_fd, &sig_info, sizeof(struct signalfd_siginfo));
if (read_size != sizeof(struct signalfd_siginfo)) {
fprintf(stderr, "read signalfd error.\n");
break;
}
if (sig_info.ssi_signo == SIGINT) {
printf("got SIGINT signal.\n");
} else if (sig_info.ssi_signo == SIGQUIT) {
printf("got SIGQUIT signal.\n");
break;
} else {
fprintf(stderr, "got unexpected signal.\n");
}
}
close(sig_fd);
return (0);
}
上面的程序中,我们将关心的两个信号 SIGINT 和 SIGQUIT 加入信号集中,随后通过 sigprocmask 将这两个信号阻塞。这里和使用 signal 或 sigaction 处理信号的方式不同,它们要求关心的信号不能被阻塞,而使用 signalfd 时,如果不阻塞这些信号,则当信号发生时会转到默认的信号处理函数中。
当信号发生时,虽然这些信号被阻塞了,但系统会将与这些信号关联的文件置为可读状态,因此我们使用 read 函数等待信号的发生。使用这种方式,SIGINT 和 SIGQUIT 在一个线程里被串行处理。该程序在运行后,首先通过 ps 命令查看其结果如下:
# ./Application_of_Signalfd &
# ps
NAME FATHER STAT PID GRP MEMORY UID GID USER
----------------------- --------------- ---- ----- ----- ---------- ----- ----- ------
kernel <orphan> R 0 0 16KB 0 0 root
Application_of_Signalfd <orphan> R 124 124 232KB 0 0 root
测试程序的名称为 sigfd_test,其 PID 为 124(PID 的值根据实际情况会发生变化),下面我们使用 kill 命令向该进程发送关心的两个信号,SIGINT 对应的编号为 2,SIGQUIT 对应的编号为 3,结果如下:
# kill -n 2 124
got SIGINT signal.
# kill -n 2 124
got SIGINT signal.
在 Linux 下,我们使用 Ctrl+C 组合键即可给当前进程发送 SIGINT 信号,使用 Ctrl+\ 组合键即可对当前进程发送 SIGQUIT 信号。在 SylixOS 中,使用 Ctrl+C 总是使当前进程退出,而不会输出上面的信息。