文件 IO

本部分讲述的内容是不带缓冲的、以文件描述符为基准的 IO,这与标准 IO 库中带有缓冲的、以 FILE\* 为基准的 IO 形成了区分。

文件描述符就是一个非负整数,文件描述符的范围为 0\~OPEN_MAX-1,现在 OPEN_MAX 为 63(也就是说进程最多能打开 64 个文件)。当进程创建时,系统自动为其打开三个文件描述符,这三个文件描述符分别是:

文件描述符解释

0

STDIN_FILENO

标准输入

1

STDOUT_FILENO

标准输出

2

STDERR_FILENO

标准错误

另外,当打开一个文件时,系统会使用当前可用的最低文件描述符,这样,我们可以通过先关闭 STDOUT_FILENO,然后再打开文件的形式将标准输出重定向到文件中

内核使用三个数据结构用来描述进程打开的文件:进程表项、文件表项、 v 节点表项

  • v 节点表项是文件在物理磁盘中的索引。当文件第一次被打开时,系统将其载入内存

  • 文件表项由内核维护,是进程共享的,包含的三个字段用来描述文件打开的状态:文件状态标志、当前文件偏移量、v 节点指针

  • 进程表项是进程私有的,其将文件描述符映射到文件指针上,而文件指针指向了文件表项

dup

dup 函数用于复制一个已有的文件描述符,其函数原型为:

int dup(int fd);
int dup2(int fd1, int fd2);

dup 函数复制 fd 并将复制后的文件描述符返回。dup2 将 fd1 的描述符复制到 fd2,如果 fd2 已经打开,则先关闭 fd2,如果 fd1 == fd2,则直接返回。

复制后的文件描述符和以前的文件描述符只是文件描述符一样,其指向的文件表项相同

/dev/fd

本文的场景为 Linux,Unix 暂不考虑

在 Linux 中提供了 /dev/fd 目录,此目录中的目录项为当前进程打开的所有文件描述符。由于 Linux “一切皆文件的思想”,此目录中的目录项是指向底层物理文件的符号链接,直接打开 /dev/fd 中的目录项相当于 dup 函数,且新文件描述符的属性与原有描述符无关(这点和 Unix 不同)

另外,Linux 还创建了 /dev/stdin, /dev/stdout, /dev/stdout 三个符号链接。在终端中可以很方便地使用它们。例如将消息发送到 stderr :

echo 'hello' > /dev/stderr

或是 :

echo 'hello' > /dev/fd/3

C++ 可以通过打开 /dev/fd 的形式读写文件,例如 :

std::ofstream os("/dev/fd/1");
os<<"hello";

会将消息发送到 stdout

打开文件

调用 open 或者 openat 可以打开一个文件:

int open(const char* path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char* path, int oflag, ... /* mode_t mode */);

fd 用来代指 path 的起始路径,当 fd 具有 AT_FDCWD 属性时,openat 和 open 函数并无差距。另外, open 和 openat 总是选择当前可用的最小的文件描述符

常量 _POSIX_NO_TRUNC 决定了当文件名太长时函数出错还是截断路径。一般而言,现代操作系统允许的最长路径为 255 个字符

oflag 指定了打开文件时的行为,主要有:

标志作用

O_RDONLY

只读打开

O_WRONLY

只写打开

O_RDWR

读写打开

O_EXEC

只执行打开

O_APPEND

每次写时追加到文件末端

O_CLOEXEC

把 FD_CLOEXEC 设置为文件描述符标志

O_CREAT

文件不存在时则创建

O_DIRECTORY

若 path 不是路径则报错

O_EXCL

如果同时指定了 O_CREAT 而文件又存在,则报错,否则创建新文件。此操作可用来确定文件是否存在,且是一个原子操作

O_NOCTTY

若 path 指向的是终端设备,则不将此设备作为此进程的控制终端

O_NONBLOCK

将文件设置为非阻塞模式

O_SYNC

同步写文件内容和文件属性

O_TRUNC

截断文件

O_TTY_INIT

O_DSYNC

同步写文件内容

O_RSYNC

使所有以 fd 为参数的 write 操作等待至对文件的同一部分写操作完成

另外,当指定 O_CREAT 选项时,mode 参数必须指定,此参数用来说明新建文件的权限位

关闭文件可以使用 close:

int close(int fd);
  • 当关闭文件时会自动释放进程在此文件上持有的记录锁

  • 当进程终止时,内核会自动关闭它打开的文件

很多程序利用上述第二个特性自动关闭文件,但是这里并不建议,因为当你的程序被脚本循环调用时可能会导致占用大量文件句柄

fcntl

fcntl 用来更改已打开文件的属性:

int fcntl(int fd, int cmd, ...);

fcntl 最后一个参数有两种形式,要么是 int,要么是一个指向结构体的指针。当为 int 时含义即为 oflags

根据 cmd 参数,fcntl 具有以下能力:

功能gettersetter

复制文件描述符

F_DUPFD/F_DUPFD_CLOEXEC

设置文件描述符

F_GETFD

F_SETFD

设置文件标志

F_GETFL

F_SETFL

获取异步 I/O 所有权

F_GETOWN

F_SETOWN

设置记录锁

F_SETLK

F_SETLK/F_SETLKW

对于 F_DUPFD 而言,返回的文件描述符是大于/等于第三个参数的最小值,新的文件描述符与原文件描述符的描述符标志不同,其 F_CLOEXEC 标志被清除

lseek

程序持有的每个文件句柄都有一个与其相关联的文件偏移量属性,其代表了下次读写文件时开始的位置。可以使用 lseek 改变此属性:

off_t lseek(int fd, off_t offset, int whence);

此函数会将文件指针定位到 whence + offset 的位置,whence 的取值为:

取值含义

SEEK_SET

文件开头

SEEK_CUR

文件当前偏移位置

SEEK_END

文件末尾

若 lseek 成功,则返回新的文件偏移位置,否则返回 -1。对于管道、FIFO、套接字这些无法设置偏移量的文件而言,还会置 error = ESPIPE。另外,文件偏移位置:

  • 某些文件允许负的偏移位置

  • 文件偏移量允许超过文件末尾

在第二种情况下,会生成文件空洞,空洞部分不占用任何磁盘空间,并默认为 0。如果复制一个含空洞的文件,则新文件的空洞位置会占用磁盘空间

Intel x86 CPU 下 FreeBSD 的 /dev/kmem 允许负的偏移量

读写数据

读写数据需要用到两个函数:

ssize_t read(int fd, void* buf, size_t nbytes);
ssize_t write(int fd, const void* buf, size_t nbytes);

与 size_t 相比而言,ssize_t 允许返回负数

对于两个函数而言,参数 nbytes 代表了 buf 的大小。当函数成功时返回读出的字节数,失败时返回 -1。read 从文件向 buf 写数据,write 从 buf 读出数据

另外,以下情况会导致 read 返回的字节数少于 nbytes:

  • 已经到达文件末端

  • 剩余数据不足

  • 终端设备一般每次只允许读一行

  • 面向记录的设备一次只允许读一个记录

  • 信号中断

write 成功时返回值与 nbytes 相等,否则出错。一般 write 失败的原因是:

  • 磁盘已满

  • 文件长度超出限制

当缓冲区大小与磁盘扇区大小相等时,磁盘的效率一般最高。

如果后台进程尝试读写控制终端时,会触发 SIGTTIN/SIGTTOU 信号,这会导致 read/write 函数失败。简单的来说,如果 fork 了一个读控制台的 cat,然后父进程进入后台,则 cat 会变为僵死进程。

离散读和聚集写

离散读 (Scatter Read)聚集写 (Gather Write) 使得可以在一次函数调用中读写多个缓冲区,其函数签名为:

ssize_t readv(int fd, const iovec* iov, int iovcnt);
ssize_t writev(int fd, const iovec* iov, int iovcnt);

其中,iovec 的定义为:

struct iovec{
   void* iov_base;
   size_t iov_len;
};

也就是说 iov 实际上是一个数组的数组。参数 iovcnt 指明了 iov 的大小。两个函数都是依次对缓冲区进行访问,当一个缓冲区完全访问后才访问下一个。

fallocate

Linux 亦有与 fallocate 同名的二进制工具,其用法大致类似于:

fallocate -c -o offset -l length filename

在 Linux 内核版本大于 v3.15 的 ext4/xfs 实现中,使用 fallocate 允许截断文件的一部分,例如:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FALLOC_FL_COLLAPSE_RANGE
    #define FALLOC_FL_COLLAPSE_RANGE 0x08
#endif

int main(int argc, const char* argv[]) {
    int   ret;
    char* page = malloc(4096);
    int   fd   = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);

    if(fd == -1) {
        free(page);
        return (-1);
    }

    // Page A
    printf("Write page A
");
    memset(page, 'A', 4096);
    write(fd, page, 4096);

    // Page B
    printf("Write page B
");
    memset(page, 'B', 4096);
    write(fd, page, 4096);

    // Remove page A
    ret = fallocate(fd, FALLOC_FL_COLLAPSE_RANGE, 0, 4096);
    printf("Page A should be removed, ret = %d
", ret);

    close(fd);
    free(page);

    return (0);
}

使用轮询的非阻塞 IO

我们可以看一下以轮询的方式查询如何从 stdin 读取数据:

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    assert(flags >= 0);
    flags |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flags);
    char buf[255];
    int  nRead = 0;
    while(nRead != 2) {
        errno = 0;
        nRead = read(STDIN_FILENO, buf, 255);
        fprintf(stderr, "nRead = %d, errno = %d\n", nRead, errno);
        if(errno == EAGAIN) {
            puts("没有数据");
            continue;
        }
        if(errno == 0) write(STDOUT_FILENO, buf, nRead);
    }
    return 0;
}

当 stdin 没有数据时。read 返回 -1,error == EAGAIN。当没有数据时我们一直进行轮询。但是由于不清楚 stdin 到底有多少数据,因此我们规定当读取到的数据数量为 2 时跳出循环(换行符也计算在内)

IO 多路复用

select

select 是 IO 多路复用的一种 API,其典型特点是构造三个 1024 容量的数组,在其中保存了需要观察的文件描述符。每次调用 select 就会对这三个数组进行一次遍历。这三个数组分别是 readfds、writefds 和 exceptfds。另外,为了节省空间,这三个数组使用位图的形式实现。select 的 API 为:

int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, timeval* tvptr);

正如上述所言,readfds 用来储存需要读取的文件描述符,writefds 用来储存需要写入的文件描述符,exceptfds 用来储存发生异常的文件描述符,tvptr 用来表示 select 最高允许等待的时间。而 maxfdp 用来表示需要遍历的范围,其值为最大文件描述符加一,如果不指明的话,select 将会遍历 1024 的元素。

select 会返回已经准备好的描述符的数量,返回 -1 说明出错。描述符准备好的含义对于上述三个 fd_set 而言分别是是可读、可写、含未决异常。如果一个描述符已经准备好了读和写,那么会进行两次计数。

fd_set 是一个位图,对其修改需要使用特定的函数,这些函数根据实现可能被实现为宏:

int FD_ISSET(int fd, fd_set* fdset); // 若 fd 在 fdset 中,返回非零值

int FD_CLR(int fd, fd_set* fdset);
int FD_SET(int fd, fd_set* fdset);
int FD_ZERO(fd_set* fdset);

当三个 fd_set 指针均为 NULL 时,select 提供了比 sleep 更加精细的休眠功能。而当 tvptr == NULL 时,则表明阻塞等待至有文件描述符准备好。

另外,select 还有一个变体 pselect 来提供更加精细的定时功能:

int pselect(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const timespec* tvptr, sigset_t* sigmask);

使用 sigmask 可以屏蔽指定信号

然后是使用 select 实现的非阻塞 IO:

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <strings.h>
#include <sys/select.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    assert(flags >= 0);
    flags |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flags);

    fd_set readFds;
    fd_set excFds;
    FD_ZERO(&readFds);
    FD_ZERO(&excFds);
    char buf[255];
    int  ret;
    while(1) {
        bzero(buf, 255);
        FD_SET(STDIN_FILENO, &readFds);
        FD_SET(STDIN_FILENO, &excFds);
        ret = select(STDIN_FILENO + 1, &readFds, NULL, &excFds, NULL);
        assert(ret >= 0);
        if(FD_ISSET(STDIN_FILENO, &readFds)) {
            read(STDIN_FILENO, buf, 255);
            write(STDOUT_FILENO, buf, 255);
        } else if(FD_ISSET(STDIN_FILENO, &excFds)) {
            fprintf(stderr, "errno = %d", errno);
        }
    }
    return 0;
}

另外,除了用阻塞方式的 select 外,也可以将 timeval 设置为 \{0, 0},这样 select 会立即返回。这种方式常用来测试一个文件描述符是否可读/可写

poll

poll 是另一种 IO 多路复用的手段,相比 select 最大支持 1024 个文件描述符而言,poll 支持的文件描述符是无穷的:

int poll(pollfd* fdarray, nfds_t nfds, int timeout);

poll 底层使用链表储存 fd

其中,pollfd 的定义为:

struct pollfd{
   int fd; // 需要监控的文件描述符
   short events; // 需要监控的事件
   short revents; // 实际发生的事件
};

pollfd.revents 在 poll 返回时由内核更改,用户无需设定。

其中,events 为:

标志写入 events ?写入 revents ?描述

POLLIN

\(\checkmark\)

\(\checkmark\)

可以不阻塞地读取高优先级以外的数据

POLLRDNORM

\(\checkmark\)

\(\checkmark\)

可以不阻塞地读取普通数据

POLLRDBAND

\(\checkmark\)

\(\checkmark\)

可以不阻塞地读取优先级数据

POLLPRI

\(\checkmark\)

\(\checkmark\)

可以不阻塞地读取高优先级数据

POLLOUT

\(\checkmark\)

\(\checkmark\)

可以不阻塞地写普通数据

POLLWRNORM

\(\checkmark\)

\(\checkmark\)

同上

POLLWRBAND

\(\checkmark\)

\(\checkmark\)

可以不阻塞地写优先级数据

POLLERR

\(\checkmark\)

已出错

POLLHUP

\(\checkmark\)

已挂断

POLLNVAL

\(\checkmark\)

描述符没有引用一个文件

文件描述符被挂断后依然可读

事件与 select 略有不同:

含义

-1

永远等待

0

立即返回

非零值

等待 timeout 毫秒

使用 poll 实现的非阻塞 IO 方式为:

#include <assert.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    assert(flags >= 0);
    flags |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flags);

    int            nfds = 1;
    struct pollfd* pfds;
    pfds = calloc(nfds, sizeof(struct pollfd));
    assert(pfds != NULL);
    pfds[0].fd = STDIN_FILENO;
    pfds[0].events |= POLLIN;
    while(1) {
        int ready;
        ready = poll(pfds, 1, -1);
        assert(ready != -1);
        fprintf(stderr, "Ready: %d\n", ready);

        char buf[255];
        memset(buf, '\0', 255);
        for(int i = 0; i < nfds; ++i) {
            if(pfds[i].revents & POLLIN) {
                ssize_t l = read(pfds[i].fd, buf, 255);
                write(STDOUT_FILENO, buf, 255);
            }
        }
    }
    return 0;
}

epoll

epoll (Enhanced poll) API 的行为与 poll 类似:监控多个文件描述符以查看其上可用的 IO。epoll 分为边缘触发和水平触发。

  • 另一方面,由于 select 和 poll 都是使用的轮询的方式,导致当文件描述符很多时性能较低,而 epoll 底层使用了红黑树来保证效率

  • 更准确地来说,epoll 监控的是可读可写事件

epoll 的核心概念是 epoll 实例:一个内核中数据结构,包含了两个列表:

  • 兴趣列表(也叫做 epoll set):包含了需要监控的文件描述符

  • 就绪列表:包含了兴趣列表中已经就绪的文件描述符。就绪列表是内核根据当前 IO 状况动态创建的

以下 API 用于管理 epoll 实例:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, epoll_event* event);
int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout);

epoll_create 用来创建一个 epoll 实例,并返回一个指向此实例的文件描述符

epoll_ctl 用来向 epoll 示例中添加、删除、更改感兴趣的文件描述符,op 的参数为:

命令描述

EPOLL_CTL_ADD

增加要监视的文件描述符

EPOLL_CTL_MOD

更改目标文件描述符的事件

EPOLL_CTL_DEL

删除要监视的文件描述符,event 参数会被忽略,可以传入 NULL

epoll_wait 在没有可用文件描述符时阻塞当前线程。当有可用文件描述符时将其写入 events

其中 epoll_event 的定义为:

struct epoll_event {
   uint32_t events; // 与 poll 能监视的事件差不多,只是宏名前面加了个E
   epoll_data_t data; // 用户数据,除了能保存文件描述符以外,还能保存一些其它有关数据
};

typedef union epoll_data {
   void        *ptr;
   int          fd;
   uint32_t     u32;
   uint64_t     u64;
} epoll_data_t;

epoll_wait 用来等待文件描述符可用。当其成功返回时,可以通过 events→data→fd 拿到可用的文件描述符

水平触发和边缘触发

假设有以下事件发生:

  • 一个代表管道的文件描述符 rfd 被添加到兴趣列表

  • 管道内在写端被写入了 2kB 数据

  • epoll_wait 将 rfd 写入到就绪列表中

  • 管道读端读了 1kB 的数据

  • epoll_wait 再次被调用

则:

  • 如果 rfd 是边缘触发。则第五步时的就绪列表中不会包含 rfd,因为边缘触发只在文件描述符状态变化时才将其加入就绪列表

  • 如果 rfd 是水平触发。则第五步时返回的就绪列表中仍会包含 rfd,因为水平触发会在文件描述符可用时将其加入就绪列表

边缘触发要求文件描述符是非阻塞的

使用 epoll 的非阻塞 IO 例子为:

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/epoll.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    assert(flags >= 0);
    flags |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flags);

    int epfd = epoll_create1(0);
    assert(epfd != -1);
    struct epoll_event ev;
    struct epoll_event events[10];
    ev.events  = EPOLLIN | EPOLLET;
    ev.data.fd = STDIN_FILENO;
    int rtn    = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    assert(rtn != -1);

    int  nfds = 0;
    char buf[255];
    while(1) {
        nfds = epoll_wait(epfd, events, 10, -1);
        assert(nfds != -1);
        for(size_t i = 0; i < nfds; ++i) {
            int fd = events[i].data.fd;
            bzero(buf, 255);
            read(fd, buf, 255);
            write(STDOUT_FILENO, buf, 255);
        }
    }
    return 0;
}

epoll 的五种写法

epoll 有六种常用写法:

  • 单线程 accept,多线程 recv/send

  • 多线程 accept,多线程 recv/send

    对于多线程而言,监听一个端口的方式是:

    listen(fd)
    
    pthread_create(thid, NULL, cd, &fd);
    pthread_create(thid, NULL, cd, &fd);
    pthread_create(thid, NULL, cd, &fd);
    pthread_create(thid, NULL, cd, &fd);

    多线程处理同一个 listenfd,应当使用水平触发

    多线程 Listen 的另一个问题是当链接进入时,会将所有的 listen 进程唤醒,解决的办法是在 epoll_wait 之前加锁。每次请求进入时只唤醒一个进程

  • 多线程 epoll_wait,不区分 accept 和 recv/send

  • 多进程 epoll_wait,不区分 accept 和 recv/send

  • master 进程 accept,工作进程 recv/send

  • 多进程适用于 session 是独立的,前后不关联的情况。比如即时通信是长链接,session 不是独立的就不推荐多进程模型

  • Nginx 使用的是第三种模型

  • 最后一种只是理论上,但是因为 fd 没法在进程间传递(fd 创建于 fork 之后),所以实际上没人用

mmap

mmap 可以将文件的一部分映射到内存中。之后程序对此内存的更改会由操作系统负责写回到磁盘上。尽管程序在写数据的时候可以超过映射文件的大小,但是超过的部分会被忽略掉。

mmap 将文件映射到进程的文件映射区,fork 的子进程会得到副本,但是执行 exec 的进程不会

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

mmap 将文件 fd 中的 [offset, offset + length] 部分映射到地址 addr 处。addr 如果置空则意味着让操作系统选择。函数的返回值即文件映射后指向的内存

对于 Linux 而言,addr 是一个 hint 类型的参数。也就是说,mmap 不一定会将文件映射到 addr 处,一切应以返回值为准

prot 表明了映射区的权限,其权限不得超过 fd 拥有的权限:

命令

含义

PROT_READ

映射区可读

PROT_WRITE

映射区可写

PROT_EXEC

映射区可执行

PROT_NONE

映射区不可访问

另外,映射区的权限后面还可以使用 mprotect 更改:

int mprotect(void *addr, size_t len, int prot);

flags 指明了内核是否回写和是否进程共享的问题:

标志含义

MAP_SHARED

共享映射。对映射区的更改是进程间可见的,而且会回写到底层文件

MAP_SHARED_VALIDATE

与 MAP_SHARED 类似,但是会无效的 flag 会导致 mmap 失败

MAP_PRIVATE

对映射区的更改不会影响底层文件

  • 在进程结束时需要使用 munmap 取消映射

  • 映射区的大小不能超过文件大小,超过部分会被忽略

  • flags 在不同版本的内核中变化比较大,使用前应当先查阅手册

另外,你还可以通过 msync 强制系统将内容回写到文件:

int msync(void *addr, size_t length, int flags);

flags 的参数为:

标志

含义

MS_ASYNC

请求回写。函数会立即返回

MS_SYNC

请求阻塞至回写完成

MS_INVALIDATE

使其他人对此文件的映射失效(以便刷新文件)

signalfd

TODO

timerfd

timerfd 是 repeat 定时器的一种。

  • timerfd 支持 read, poll 操作

  • 当 timerfd 超时时可读,读出来的是超时的次数,read 后 timerfd 会重新重新计时

    当 timerfd 和 select 结合使用时,一定要注意 timerfd 可读后进行一次 read,否者会一直触发可读事件

  • timerfd 的定时精度要高于 select 定时器的精度

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);

clockid 有两种参数:

参数含义

CLOCK_REALTIME

系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变

CLOCK_MONOTONIC

从系统启动这一刻起开始计时,不受系统时间被用户改变的影响

对于 timerfd_settime 中的 flags 有两种情况:

含义

0

相对时间

TFD_TIMER_ABSTIME

绝对时间

itimerspec 的结构为:

struct itimerspec {
   struct timespec it_interval;  // 定时器间隔
   struct timespec it_value;     // 第一次超时时间
};

下面是一个使用 timerfd 的例子:

int timerfd = timerfd_create(CLOCK_REALTIME, 0);
if(timerfd == -1) {
   spdlog::critical("创建定时器失败: {}", strerror(errno));
   exit(errno);
}

int      ret = -1;
timespec now;

ret = clock_gettime(CLOCK_MONOTONIC, &now);
if(ret == -1) {
   spdlog::critical("获取当前系统时失败:{}", strerror(errno));
   exit(errno);
}

itimerspec new_value;
new_value.it_value.tv_sec     = now.tv_sec + kTimerInterval;
new_value.it_value.tv_nsec    = now.tv_nsec;
new_value.it_interval.tv_sec  = kTimerInterval;
new_value.it_interval.tv_nsec = 0;

ret = timerfdsettime(timerfd, TFD_TIMER_ABSTIME, &new_value, nullptr);
if(ret == -1) {
   spdlog::critical("设置 timerfd 时失败:{}", strerror(errno));
   exit(errno);
}

// 直接读取或者使用 select
read(timerfd);

inotify

inotify 可以用来监控文件变化。但文件发生改变时,将会产生系统调用,inotify 通过 hack 这些系统调用来监控文件系统的变更。inotify 不支持 FUSE 文件系统,更通俗地来讲是不支持网络文件系统和虚拟文件系统(例如 samba 挂载和 /proc 虚拟文件系统)

在 inotify-tools 软件包里提供了一些工具可以用来监控文件操作。例如如果我们打算监控 /srv/test 文件上的操作,只需要执行 :

$ inotifywait -rme modify,attrib,move,close_write,create,delete,delete_self /srv/test

fanotify

fanotify 是 inotify 的更完善版本,其在 inotify 基础上添加了更多的功能,使得监听者实现了从“监听”到“监控”的跨越,同时也扩展了其应用的范围

fswatch

fswatch 是一个命令行工具,提供了类似 inotify 的跨平台的文件监视工具,例如:

fswatch -o src | xargs -n1 -I{} bash -c "cargo build"

memfd

TODO

Last moify: 2023-11-08 07:30:32
Build time:2025-07-18 09:41:42
Powered By asphinx