文件 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 具有以下能力:
功能 | getter | setter |
---|---|---|
复制文件描述符 | 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 分为边缘触发和水平触发。
|
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
|
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 | 对映射区的更改不会影响底层文件 |
|
另外,你还可以通过 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