经过 3 个月的休(tou)息(lan),是时候写点东西了……
eventfd
较新版本的 Linux 内核(2.6.22 之后)提供了一个新的系统调用 eventfd() 来实现事件通知(参考资料 [1]):
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
函数返回一个可用于 read()/write()/epoll()/close() 的 fd,fd 包含一个由内核维护的 8 字节的无符号整数,初始值由第一个参数 initval 制定。write() 的 buffer 大小都限定为 8 字节,如果 buffer 大小不等于 8 字节则会返回错误,如果提供给 read() 的大小小于 8 字节也会报错。每次对 fd 执行 write() 的时候相当于这个值加上写入的 buffer 的值,而 read() 的时候则会把这个值读到 buffer 中,且原来的值会被清零(未设置 EFD_SEMAPHORE)或者减 1(设置了 EFD_SEMAPHORE)。当值被清零后,如果 fd 没有设置非阻塞,继续调用 read() 会被阻塞。
第二个参数是一些标记,可以是下面这些值的或组合:
- EFD_CLOEXEC:设置 close-on-exec 描述符,即执行 exec() 之后,之前的 fd 会自动关掉;
- EFD_NONBLOCK:设置 read() 的时候为非阻塞模式,这样值被清零后 read() 立刻返回;
- EFD_SEMAPHORE:控制 read() 的行为。如果打开了这个标志,read()/write() 类似信号量的行为,每次 read() 只对变量值减 1;否则会把变量值清零。
当不再需要这个 fd 时可用 close() 释放资源。
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/eventfd.h>
int main(void)
{
int nbytes;
unsigned long value = 5;
unsigned long res = 0;
int fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
nbytes = write(fd, &value, sizeof(value));
printf("first write %d bytes.\n", nbytes);
nbytes = write(fd, &value, sizeof(value));
printf("second write %d bytes.\n", nbytes);
nbytes = write(fd, &value, sizeof(value));
printf("third write %d bytes.\n", nbytes);
nbytes = read(fd, &res, sizeof(res));
printf("read %d bytes, res = %lu.\n", nbytes, res);
nbytes = read(fd, &res, sizeof(res));
printf("read %d bytes, errmsge -> %s.\n", nbytes, strerror(errno));
close(fd);
return 0;
}
timerfd
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);
这三个函数分别对应 timer_create(),timer_settime() 和 timer_gettime()。不同之处在于,这三个函数返回的是一个 fd,和普通文件的 fd 类似,可用于 read()/epoll()/close()。
timerfd_create() 的第一个参数取值为 CLOCK_REALTIME 或 CLOCK_MONOTONIC。这两个参数取值从网上找了好几圈都没怎么看明白,下面是参考了参考资料 [4] 的内容结合实验得到的结论:
这两个参数主要是在调用 clock_gettime() 的时候使用的。调用 timer_settime() 的参数可以通过调用 clock_gettime() 得到,而 clock_gettime() 的参数要和 timerfd_create() 的参数对应。CLOCK_REALTIME 表示相对时间,即获取的时间是从 1970.1.1 到现在的时间,如果更改系统时间,那么获取到的时间值也不一样;CLOCK_MONOTONIC 是绝对时间,表示从电脑启动到目前为止的时间,和系统的时间没关系。
另外参考资料 [4] 提到的一个细节是,tv_nsec加上去后要判断是否超出 1000000000(如果超过 tv_sev 要加 1),否则会设置失败。
timerfd_create() 的第二个参数用于设置 fd 的一些属性,可取值有 TFD_NONBLOCK 和 TFD_CLOEXEC,含义和 eventfd() 一样。
其余两个函数用到了下面两个结构体:
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};
struct itimerspec {
struct timespec it_interval; /* Interval for periodic timer */
struct timespec it_value; /* Initial expiration */
};
timerfd_gettime() 获取 fd 指向的定时器的状态。其中 it_value 的值是距离下次触发剩下的时间,it_invertal 返回间隔时间。
timerfd_settime() 的第二个参数取值可能为 0 或者 TFD_TIMER_ABSTIME,分别表示设置超时类型是相对时间还是绝对时间。如果取值为 0,表示下一次的超时时间为当前时间加上 new_value.it_value 的值;如果为 TFD_TIMER_ABSTIME 表示超时时间点就是 new_value.it_value 的值。如果 old_value 不为 NULL 则返回旧的设置,返回值同 timerfd_gettime()。如果 new_value.it_value 的两个域都为 0 表示停掉这个定时器;如果 new_value.it_interval 的两个域均为 0 表示这个定时器只触发一次。
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/timerfd.h>
int main(void)
{
uint64_t v = 0;
struct itimerspec new_value = {
.it_value = {
.tv_sec = 10,
},
.it_interval = {
.tv_sec = 1,
},
};
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (timerfd_settime(fd, 0, &new_value, NULL) != 0) {
fprintf(stderr, "timerfd_settime error\n");
return -1;
}
while (1) {
while ((read(fd, &v, sizeof(v)) <= 0)) {
struct itimerspec t;
timerfd_gettime(fd, &t);
fprintf(stderr, " time left: %lu.%lu\n", t.it_value.tv_sec, t.it_value.tv_nsec);
sleep(1);
}
printf("expired. read value = %lu\n", v);
}
return 0;
}
调用 read() 时,如果已经超时立即返回,否则会阻塞(没有设置 TFD_NONBLOCK的情况下)。读到的值是一个 8 字节的整数,表示已经触发超时的次数。
signalfd
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
这个函数可以用来代替 sigwaitinfo() 或者 sigaction()/signal() 中的信号处理回调函数。如果第一个参数 fd 是 -1,signalfd() 会创建一个新的 fd 并且和信号集 mask 关联,如果 fd 是一个有效的 signal fd,则会使用新的 mask 替换原来的信号集。第三个参数和上面介绍的函数类似,可以取值为 SFD_NONBLOCK 或 SFD_CLOEXEC。
第二个参数设置这个 fd 可以接收哪些信号,这些信号通过 sigsetops(3) 来生成。
如果失败函数返回 -1,成功返回有效的 fd(新的 fd 或者要设置的 fd)。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <sys/signalfd.h>
int main(void)
{
int fd;
sigset_t signals;
struct signalfd_siginfo info;
sigemptyset(&signals);
sigaddset(&signals, SIGINT);
if (sigprocmask(SIG_BLOCK, &signals, NULL) == -1) {
fprintf(stderr, "sigprocmask error: %s.\n", strerror(errno));
return -1;
}
fd = signalfd(-1, &signals, 0);
if (fd < 0) {
fprintf(stderr, "signalfd failed: %s.\n", strerror(errno));
return -1;
}
int nbytes = read(fd, &info, sizeof(info));
if (nbytes == sizeof(info))
fprintf(stderr, "SIGINT captured.\n");
close(fd);
return 0;
}
要捕获的信号集合通过 sigsetops(3) 系列函数来操作。在调用 signalfd() 前需要先调用 sigprocmask() 对需要捕获的信号进行阻塞,要不这个信号就会被捕获从而执行默认的行为(例如 Ctrl-c 就直接结束程序,后面的输出都没机会执行)。设置好之后就可以按照一般的读写文件流程继续进行了。目前还没用到这个函数,对于 struct signalfd_siginfo 的内容没有仔细研究,先略过了。
参考资料
[1] eventfd(2)
[2] timerfd_create(2)
[3] signalfd(2)
[4] linux新API---timerfd的使用方法