前言

inotify 是一种常见的文件系统事件监听方法,Boost.Asio 是一个常见的 c++ 网络与异步 I/O 库。
它们有结合在一起使用的可能性吗?

有一个奇怪的需求:我想在一个本来使用了 asio 的应用中额外引入对 inotify 机制的使用,但 inotify 需要额外持续 read() 一个 fd ,这意味着需要额外启一个线程;额外启一个线程是不理想的,可能引入新的同步问题。所以有没有一种方法,能够将 inotify 的 fd 与 asio 的异步操作机制相结合,规避新线程的创建呢?

调查 asio

(↓ 这一部分牵扯到的代码全部 using namespace boost::asio ,为了方便就省略掉命名空间前缀了)

很奇怪的是,asio 作为网络库是知名的,但若只是使用其基本的异步 I/O 功能,在网上却很难找到相关资料。而且,asio 的官方文档实在是简陋到有点幽默了。

犄角旮旯的地方找到了一份 ppt ,里面描述了使用 asio 进行基本文件 I/O 操作的方式:

并且,其中还提到:

虽然 asio 提供了通用的网络操作 api ,但似乎没有提供通用的文件操作 api 。
也许,这种不通用性便是 asio 在异步文件操作上没有被广泛使用的原因吧。
其实我很好奇为什么网络 api 可以被通用化,而文件 api 却不行,在 unix 平台上它们都是类似的 fd ,在 windows 上难道它们差异很大吗?

就不深入研究这种平台差异了,先来聚焦于 POSIX 上 asio 的文件操作,来看看里面牵扯到的各个东西是如何作用的。

io_service ioservice;
posix::stream_descriptor stream{ioservice, STDOUT_FILENO};
auto handler = [](const boost::system::error_code&, std::size_t) {
std::cout << ", world!\n";
};
async_write(stream, buffer("Hello"), handler);
ioservice.run();

(其实现在 asio 是支持跨平台的文件读写操作的,见下面的“通用文件操作 api”部分,不过这个东西可能是新版加入的,之前没有?)

io_context

io_service 在新版 asio 库中已被 io_context 取代,它是一个 I/O 事件的复用/解复用器。

在说到 I/O 复用时,我们先来设想下面这个场景:我有两个 fd ,分别是 A 和 B ,它们随时都可能有数据过来,因此我需要持续监听它们。正常的监听就是去 read() ,或者说,blocked read ,也就是在数据到来之前 read() 是不会返回的。既然我有两个文件描述符需要监听,那是不是意味着我要同时进行两个 read() ,那我岂不是要开两个线程?

为了优化这种情况,操作系统往往会提供一系列的 I/O 复用机制,说白了就是让一个线程能够监听多个文件描述符,那么它是怎么做到的呢?
以 linux 上的 epoll 为例:
1、首先,创建一个 epoll fd 。
2、将我们需要监听的 A 和 B 绑定到这个 epoll fd。
3、对这个 epoll fd 执行 epoll_wait()
4、当 A 或 B 发生 I/O 事件时(比如可以读到东西了),epoll_wait() 便会返回,此时我们可以根据 epoll_event 得知发生事件的 fd 是哪个,然后再去处理对应事件(比如去实际 read() 它,把数据拿出来)。

如果上面的解释还不够清晰,可以再看看这篇文章

梳理上面这个过程,会发现其实它由两部分组成:
1、复用:将需要监听的文件描述符 attach 到 epoll fd 上。
2、解复用:当事件发生时,弄清楚是哪个 fd 触发了事件,并采取对应的方式来处理事件。

于是我们就可以直观的理解 io_context :它是一个 epoll 的包装,负责处理 I/O 事件与回调之间的关系。比如你想异步读取文件描述符 A 和 B ,那么在 asio 内部,A 和 B 就都会被绑定到 io_context 内部的 epoll fd 上,并将请求交给内核处理;当 A 「或」 B 的数据准备好时,epoll_wait() 就会返回,此时 asio 需要根据事件信息,确认产生事件的 fd 是 A 还是 B ,并将事件分发给正确的回调。io_context 所做的事情,便是去维护 fd 与事件回调之间的关系,使得一批 I/O 事件能被“复用”到一个 epoll 上,epoll_wait() 返回时又能够被“解复用”到正确的回调函数上。当然,这里描述的 epoll 只是 linux 平台的特例,在不同平台上 io_context 有不同的实现,比如在 windows 上大概用得是 IOCP ?

当然,除了基本的 I/O 监听,io_context 还能实现如简单异步执行(io.post())和定时器(steady_timer)之类的高级操作(大概依靠 eventfdtimerfd 实现),所以宏观的来看,它完全可以扮演一个事件队列的角色(就像 Android 中的 Looper 一样),只不过是一个额外支持了网络和文件事件的高级事件队列。

有了上面的这些铺垫,最后一行的 ioservice.run() 就变得非常好理解,它就等价于 Looper.loop()) ,放在 asio 的语境下也就是开始在这个线程循环执行 epoll_wait() ,并在发生事件时执行对应的回调。所以,asio 确实可以利用 I/O 复用机制大大减少处理大量 I/O 事件所需的线程数,但最终一定要有一个线程负责处理所有的事件,这个线程便是执行 io.run() 的线程。在大多数 asio 程序的设计中,主线程会被用于事件处理,于是就可以看到 main() 函数的最后一行被写上了 io.run() / ioservice.run() ,正常情况下,这些 run() 都是永不返回的,事件监听和处理会一直持续进行。

wrappers

posix::stream_descriptorbuffer() 都是一层简单的包装,分别提供了对文件流和内存区域的抽象。这层抽象能够为 asio 的其它部分提供统一的接口,使其在一定程度上能够屏蔽平台差异并确保安全性,参见下方的“asio 中的 concept”。

async_write

在这一块内容中,我想要首先聚焦于如何看懂 asio 的文档。
不觉得上面这图很怪吗?它给了 async_write 作为函数模板的定义,但是,入参的类型都是模板的类型参数,那我该怎么知道这个模板能够接受什么样的参数呢?

举个例子:

template<typename T>
T plus(T t1, T t2) {
    return t1 + t2;
}

我们一眼就能知道,对于上面这个模板,只有能相加的东西作为参数传入时才能通过编译,传一个未重载 operator+() 的对象进去多半是要报错的,但是,这种 T 的所需特性并没有体现在模板的签名中,比如 T extends Addable(伪 java )。对于上面图中的 AsyncWriteStreamConstBufferSequence 也是如此,它们都只是模板的 typename ,并没有体现所需对象的特性,所以,就没有一种东西能够体现这种特性吗?

Concept

Basically ,对象的所需特性其实是一种抽象。在一般的编程语言中,这种抽象由类或接口的继承来实现。
但是 c++ 比较特殊,除了由类和虚函数实现的运行时多态外,它还有基于模板实现的编译时多态:模板基于传入的类型进行展开,然后执行编译,在编译成功/失败之前,模板本身并不能知道参数是否与模板中的语法匹配。就比如对于上面的 plus() ,模板本身并没有能力检查传入的类型是否支持 operator+() ,对所有的传入类型它都只能老老实实的展开和编译,只是没有实现 operator+() 的类型无法通过编译罢了。于是,这就提供了相当高的灵活性:任何执行 + 时不会编译出错的对象都能被填入到这个模板中,而无需关心这些对象继承了啥父类。这在别的语言如 java 中是难以实现的:毕竟 java 中可没有类似 Addable 这样的接口,想要实现这种 plus() 得为每个基本类型实现一个函数重载。

当然,除了特殊的 operator+() ,这种编译时多态也能被应用于一般的函数,比如:

struct A {
    void bar() {};
};

struct B {
    void bar() {};
};

struct C {
    void bar1() {};
};

template<typename T>
void foo(T t) {
    t.bar();
}

int main() {
    // 调用 A 的 bar()
    foo(A());
    // 调用 B 的 bar()
    foo(B());
    // 编译出错,C 类型没有 bar()
    foo(C());

    return 0;
}

任何拥有方法 bar() 的类型都可以被传入这个模板,然后顺利通过编译。与虚函数不同的是,t.bar() 具体调用哪个 bar() 是由模板展开时的类型推导决定的,而非由虚表决定,因此这种编译时多态相较运行时多态有着更好的性能;同时,这些类也不需要继承于某个基类,提供了相当高的灵活性。成功与否的唯一决定因素便是:模板展开后是否有语法错误,能否成功通过编译。

回到正题,既然这种基于模板的编译时多态也是一种抽象,那该如何表示对 T 的约束呢?比如 T 必须可相加,T 必须拥有 bar() 方法。难道必须等到模板展开后编译,依靠编译报错来判断它是否符合要求吗?我们就不能在模板展开之前拦截不符合要求的类型吗?

对此,c++ 20 引入了一套“概念”(Concept)处理机制,即将“能够相加”、“拥有 bar() 函数”等一个个通过编译的约束条件抽象成“概念”,并允许使用一系列的语法在模板展开前对“概念”进行拦截和处理。也就是说,对于 c++ 来说,运行时多态抽象出来的东西叫“基类”,而编译时多态抽象出来的东西叫“概念”。

当然,自从模板存在的那一天起,“概念”就一直存在,c++ 20 所引入的是一套优化后的概念处理机制,而非“概念”本身,比如让概念不匹配时打印出更加友好的报错,又或者为原来的 SIFNAE 提供封装,让模板匹配的逻辑变得易懂一些,这里有一篇关于 c++ 20 concept 的介绍,看起来还不错。

asio 中的 concept

对于 asio 来说,它(到目前为止)并没有使用 c++ 20 的概念处理机制。但是,这并不意味着 asio 没有对“概念”的抽象,事实上,只要点进这里的链接,就能看到其对这些类型的“概念”要求:

比如,async_write() 的第一个参数是一个 AsyncWriteStream 的概念,它必须拥有 get_executor()async_write_some() 方法。任何一个拥有这俩方法的对象,理论上都能被 async_write() 接受并通过编译。

很难受的是,我们得自己去找什么东西符合 AsyncWriteStream 的概念,asio 好像并没有一个文档来说明哪些东西支持这一概念,但是,不管怎么样,posix::stream_descriptor 一定是符合这一概念的,为 windows 平台封装的 windows::stream_handle 也是符合这一概念的。

同理,async_write() 的第二个参数必须符合 ConstBufferSequence 的概念,这一概念要求该参数能通过 buffer_sequence_begin()buffer_sequence_end() 转换为对应的迭代器。

我们上面代码中的 buffer("Hello") 其实就是将 "Hello" 所在的常量地址包装成了一个 const_buffer ,这个 const_buffer 可以通过 buffer_sequence_begin() / buffer_sequence_end() 转换为迭代器,因此 buffer("Hello") 是符合 ConstBufferSequence 概念的,它可以通过编译。

async_write() 的最后一个参数也是同理,具有特定签名的 callable object 是符合 WriteToken 概念的,于是我们的 handler 可以被成功传入。
(这里先暂且忽略其它种类的 Completion Tokens,聚焦于 callable object )

对于 handler ,其函数签名的要求为 void(error_code, size_t)error_code 代表可能出现的意外情况,比如在写入失败时,handler 也会被调用,但此时 error_code 会有值,代表失败的原因;第二个 size_t 则普通的代表写入的字节数。

也许你会注意到,async_write() 在其模板定义中还有一个参数:

这个参数并不是给用户使用的,它只在编译期间被用于辅助模板的展开过程,是 SIFNAE 机制的应用:它对第二个模板参数进行了校验,当第二个参数的类型不符合 ConstBufferSequence 的概念与约束时,及时告诉编译器不要展开此模板,而是去选择其它的模板重载。
(嗯,c++ 20 引入的概念处理机制也是来解决此类问题的,不过 asio 使用的仍是传统的 SIFNAE 方案)

async_write 到底做了什么?

This operation is implemented in terms of zero or more calls to the stream's async_write_some function, and is known as a composed operation. The program must ensure that the stream performs no other write operations (such as async_write, the stream's async_write_some function, or any other composed operations that perform writes) until this operation completes.

可以看到,async_write() 所做的,便是去多次调用 AsyncWriteStream 概念的 async_write_some() 方法,直到 buffer 的内容被全部写入到了文件流中。无论是 async_write() 还是 async_write_some() ,都会在被调用后立刻返回,不会阻塞。你可能会注意到 async_write()async_write_some() 在函数签名上没有本质区别,那是因为它们在本质上做着相同的事情,只是 async_write_some() 不会确保 buffer 的内容被全部写入到流中,而 async_write() 通过比较 buffer 大小和已写入大小,通过多次调用 async_write_some() 来实现了这一层保障。

(以下内容含有一定的猜测成分,我并没有研究过 boost 的代码 ↓ )

对于一次 async_write() ,所做的事情大概是这样的(假设传入的 handler 叫 handler ):
1、创建一个 handler1 ,调用 async_write_some()
2、在 handler1 中比较 buffer 大小和已写入大小,若未完全写入则再次执行 async_write_some() ,直到发生错误或完全写入(形成一个“递归”的异步操作链)。
3、如果发生错误,则将错误信息从 handler1 中转发到 handler 中,通知上层。
4、如果全部写入完成,则通过 handler 通知上层,并汇报写入的总字节数。

对于一次 async_write_some() ,所做的事情大概是这样的(假设传入的 handler 叫 handler1 ):
1、尝试以非阻塞模式(1)调用同步的 write() 将目标 buffer 写入目标 fd 中。
2、如果一次成功,则直接调用 handler1 ,全部结束。
3、如果没有成功,比如同步的 write() 返回了 -EAGAIN ,代表此时因为如缓冲区不足之类的原因无法写入。那么其会将目标 fd attach 到 io_context 上,监控目标 fd 的写就绪事件(EPOLLOUT),并在目标 fd 可写时再无阻塞的执行 write() ,并将写入的字节数通过 handler1 汇报给上层。

不过,我猜一般情况下是没这么复杂的,一般情况下 async_write_some() 在尝试 write() 时会直接成功,而且一次性写入了全部的内容,那就直接通过 handler 向上返回了,看似异步的写入其实做了个同步操作。只有在目标 fd 阻塞无法写入时,io_context 才会起到作用,避免阻塞以最大化效率,发挥 I/O 复用和异步操作的优势。

注释:

  • (1) 对这个地方还是有点疑问,asio 会自动将 fd 设置为 O_NONBLOCK 模式吗?还是说需要在传入 fd 前手动设置?我没有具体测过写入的情况,但在读取时,未手动设置 O_NONBLOCK 似乎对结果并没有影响,也不会阻塞 io_context 中的其它操作。我不确定这是因为 asio 会自动处理 O_NONBLOCK ,还是因为 asio 在读取前额外采用 FIONREAD ioctl 进行了判断,使得问题没有暴露出来。但在写入时,可没有这样的额外 ioctl 可以辅助判断是否阻塞啊。所以,现在我并不确定如果未将 fd 设置为 O_NONBLOCK 模式,是否会导致异步写入变身同步写入,等待进一步确认。And stackoverflow 上也有一个关于异步变同步疑问的帖子,目前还没人回答,可以一起观察一下。

看看其它东西

上面我们的分析全部是围绕那个 ppt 中的示例代码展开的,基本只分析了 async_write() ,别忘了我们想做的是去读 inotify 事件,那不妨来看看读相关的函数吧。

我想要将 asio 中异步读取相关的函数分为几类:

1、async_read(stream, buffer, handler) / stream.async_read_some(buffer, handler)
异步的顺序读取文件流,区别是 async_read_some() 只读一次,能读到多少是多少,而 async_read() 则会在内部“异步递归”执行 async_read_some() ,直到装满 buffer 或出错为止。

它们与我们之前介绍的 async_write()async_write_some() 是对称的。

posix::stream_descriptorwindows::stream_handle 支持该操作。

2、async_read_at(stream, offset, buffer, handler) / stream.async_read_some_at(offset, buffer, handler)
异步的随机读取文件流,相较于上面的顺序版本多了个 offset 可以用来指明从何开始读取。

它们与 async_write_at()async_write_some_at() 是对称的(这些是异步随机写入方法,虽然之前没有介绍)。

很有趣的是,posix::stream_descriptor 并不支持这一操作,只有 windows::random_access_handle 支持此操作。
(当然,这并不意味着 linux 没法随机访问文件,见下面的“通用文件操作 api”)

3、async_read_until(stream, dynamic_buffer, delimiter, handler)
异步的顺序读取文件流,但不根据 buffer 大小停止,而是根据 delimiter 停止。
delimiter 相当于停止标识符,是一个 char ,比如 \n
这里所需要的 buffer 与上面的不同,上面所需要的都是大小固定的 buffer ,而这里需要的是大小可变的 dynamic buffer (实现 DynamicBuffer_v1 概念)。dynamic buffer 可以利用 streambuf 创建,也可以使用类似 dynamic_buffer(string)dynamic_buffer(vector) 的方式创建,总之得传入一个可以 resize() 的容器,数据也会被实际存储在这个容器中(因此得保障好它的生命周期)。

async_read_until() 在内部调用的也是 async_read_some() ,因此支持该操作的流对象与 read some 是相同的。

通用文件操作 api

还记得上面的 Currently, File I/O is not supported in a platform independent manner 吗?

诶,仔细看了一下官方 reference ,其实 asio 完全是支持跨平台的文件操作的(可能是新版本加入的?)。

上面,我们只提及了 posix::stream_descriptor(io, fd)windows::stream_handle(io, handle)windows::random_access_handle(io, handle) 等平台特有的流对象,但 asio 实际上还提供了更通用的跨平台文件流封装:

  • stream_file(io, path, flags) —— 支持顺序读写的跨平台文件流对象,支持上面的所有顺序读写函数(async_read()async_read_some()async_read_until()async_write()async_write_some())。
  • random_access_file(io, path, flags) —— 支持随机读写的跨平台文件流对象,支持上面的随机读写函数(async_read_at()async_read_some_at()async_write_at()async_write_some_at())。

诶,那这些通用 api 和 posix::stream_descriptor 有啥区别呢?为什么 posix::stream_descriptor 不能支持随机读写呢?

我想,这是因为 posix::stream_descriptor 是为“万物皆文件”而准备的,任何一个 fd 都可以被塞进去,无论它是文件、socket、设备还是管道,这里面只有文件是能够 seek() 的,其它的很多东西是只能顺序读写的。既然是去抽象一个可以代表万物的文件描述符,那自然而然随机读写就不在考虑范围内了。

stream_filerandom_access_file 显然是简化了这一抽象,它们只能代表实际有路径的文件,拿它们和 posix::stream_descriptor 比较就好比拿 FILE *f = ...int fd = ... 比较,前者有 stdio 的封装来确保跨平台兼容性,后者却是 unix 特有的,这里所蕴含的道理是一样的。

除了跨平台的文件流封装,asio 还提供了跨平台的管道封装:
readable_pipe(io)writable_pipe(io)
它们支持单向的读或写操作。
虽然这看起来很怪,但它确实是跨平台的,可以在这里找到官方介绍。

实现:结合 inotify

终于要回到正题了,接下来看看 inotify ,再看看怎么把它结合到 asio 中去。

inotify 的基本使用是简单的,可以在 man7 上找到详细文档,也可以在 stackoverflow 上找到最简用例。

总的来说,inotify 的最简使用分成三步:
1、int fd = inotify_init() 创建一个 inotify fd 。
2、inotify_add_watch(fd, "/path/to/target/directory", 目标事件) 增加要监听的目标事件。
3、read(fd, buf, buf_size) 读取事件,将事件存入 buf 中,读到的事件是 struct inotify_event

struct inotify_event 的定义如下,它是一个特殊的变长结构体

struct inotify_event {
    int      wd;       /* Watch descriptor */
    uint32_t mask;     /* Mask describing event */
    uint32_t cookie;   /* Unique cookie associating related
                            events (for rename(2)) */
    uint32_t len;      /* Size of name field */
    char     name[];   /* Optional null-terminated name */
};

其中,len 代表了 name 的实际大小(包括 name 末尾的 \0),因此一个 inotify_event 的实际大小为 sizeof(struct inotify_event) + event->len

对于事件的读取和处理,有一些注意点:

  • 默认情况下 read() 发生的是阻塞读取,会一直阻塞到有事件为止。
  • 一次 read 可能读取到一个或多个变长的 struct inotify_event ,处理时要特别小心。
  • 选择 buf 大小时要注意,由于 struct inotify_event 是变长的,buf 至少得能够装下一个满长的 struct inotify_event ,因此 buf 空间至少需要预留 sizeof(struct inotify_event) + NAME_MAX + 1
  • 如果选择 buf 大小为 sizeof(struct inotify_event) + NAME_MAX + 1 ,也就是恰好装下一个满长事件,并不意味着一次只会读到一个事件,因为 buffer 仍然可以装下多个不满长的事件。因此,无论选择多大的 buffer 大小,都要考虑一次读到多个事件的可能性,不能一次 read() 只处理一个事件,而是要通过比较读到的总大小和单个事件的大小,确保所有事件都得到了正确的处理。

处理一次读到的多个事件,示例代码如下:

int sizeRead = read(fd, buf, sizeof(buf));
char* pos = buf;
// 直到本次读到的全部数据都被处理完为止
while (pos - buf < sizeRead)
{
    // 取出单个事件
    struct inotify_event* event =
        reinterpret_cast<struct inotify_event*>(pos);
    // 在此处理单个事件
    // 跳过本事件,后面可能还有别的事件
    pos += sizeof(struct inotify_event) + event->len;
}

好,接下来该考虑一下如何整合进 asio 里了。
首先,该选择哪个读取方法?
async_read()async_read_some()async_read_until()

首先排除 async_read_until() ,因为事件之间是没有合适的分隔符的,其次排除 async_read(),因为事件是变长的,我压根没法决定读取多少内容才算结束。

所以唯一的选择是 async_read_some()
那么又有下一个问题了,一次 async_read_some() 只代表“读一次,在有结果时调用回调”,那么我该如何实现持续监控呢?这里可不能像同步读取一样用 while(1) 啊。
诶,那么,我们就得在一次 async_read_some() 的回调中执行下一次 async_read_some() ,形成一种“异步递归”结构,当然,这种递归即使无限下去也是不会爆栈的,因为它是异步的,下一次回调时原来的栈早就析构了。

于是,最终实现的代码如下:

#include <fcntl.h>
#include <sys/inotify.h>

#include <boost/asio.hpp>

#include <functional>
#include <iostream>
#include <memory>

// 选择一个最小的缓冲区大小,恰好装下一个满事件。
static constexpr const size_t eventBufSize =
    1 * (sizeof(struct inotify_event) + NAME_MAX + 1);

// 处理单个事件
static void onEventReceived(struct inotify_event* event)
{
    std::string_view fileName(event->name);
    std::cout << fileName << std::endl;
    // 你的自定义操作
}

// async_read_some() 的原始 handler ,需要处理一次读到多个事件的情况,并安排“异步递归”
static void rawEventHandler(
    std::shared_ptr<boost::asio::posix::stream_descriptor> stream, char* buf,
    const boost::system::error_code& ec, size_t sizeRead)
{
    if (ec)
    {
        std::cerr << "Failed when reading inotify event " << ec.message()
                  << std::endl;
        return;
    }
    // 缓冲区中可能存在多个事件,逐一对其进行处理
    char* pos = buf;
    while (pos - buf < sizeRead)
    {
        struct inotify_event* event =
            reinterpret_cast<struct inotify_event*>(pos);
        onEventReceived(event);
        // inotify_event 是个可变长度结构体,必须加上 len 才能得到其实际长度
        pos += sizeof(struct inotify_event) + event->len;
    }
    // 本次读取完成,准备下次读取
    stream->async_read_some(boost::asio::buffer(buf, eventBufSize),
                            std::bind_front(rawEventHandler, stream, buf));
}

static void setupInotify(boost::asio::io_context& io)
{
    // static 或在堆上分配,确保长期有效
    // 是实际存储结果的地方
    static char buf[eventBufSize];
    // 需要确认这里到底要不要手动配置 nonblock ,见“async_write 到底做了什么?”的注释 1
    // 实测读操作没有影响,那么写呢?
    // 作为最佳实践,我觉得还是配一下比较安全
    // int fd = inotify_init();
    int fd = inotify_init1(IN_NONBLOCK);
    if (fd < 0)
    {
        std::cerr << "Unable to set up inotify" << std::endl;
        return;
    }

    // 不妨来监控这个事件
    inotify_add_watch(fd, "/home/libxzr", IN_CREATE);

    // stream 需要保持长期有效,因此不能在栈上分配,当然你也可以 static
    auto stream = std::make_shared<boost::asio::posix::stream_descriptor>(io,
                                                                          fd);
    // 将 stream 的智能指针传递到 bind 形成的闭包之内,延续其生命周期
    // async_read_some 需要固定大小的缓冲区,因此选择拿 buffer() 把上面的 buf 包一下
    stream->async_read_some(boost::asio::buffer(buf, eventBufSize),
                            std::bind_front(rawEventHandler, stream, buf));
}

int main()
{
    boost::asio::io_context io;
    setupInotify(io);
    // 理论上永不退出
    io.run();
}

编译与测试

sudo apt install libboost-system-dev
g++ -std=c++20 test.cpp
./a.out

可以看到,已经成功监听到 inotify 事件了。

也许你会好奇为什么编译时不需要 -lboost_system ,那是因为 asio 是一个 header only 的库,只需要引用头文件就可以使用其全部功能。

参考资料