🔍 本篇概要
eventfd
是Linux
提供的一種輕量級事件通知機制。你可以把它想象成一個“計數器盒子
”。- 它里面維護的是一個
64位
的計數器。 - 寫入:往盒子里放一些數字(比如 1、5、10),表示有幾件事發生了。
- 讀取:從盒子里取出數字,每取一次就減少一個(或者一次性全拿走)。
它非常適合用來做:
- 多線程/進程之間的通信。
- 異步通知(如 epoll 配合使用)。
- 控制并發資源訪問(類似信號量)。
一· 🛠 如何創建 eventfd
?
如下:
#include <sys/eventfd.h>
int efd = eventfd(initval, flags);
解釋:
參數名 | 含義 |
---|---|
initval | 初始化值(初始計數器數值) |
flags | 標志位(可選,影響行為) |
- 這里對于initval是給它設置初始值,如果不write寫入,計數器就是這個初始值,否者就是write寫入的那個值。
二.🎯 eventfd
的所有標志詳解(Flags
)
下面是 eventfd() 支持的所有標志:
標志名 | 含義 | 是否推薦使用 |
---|---|---|
EFD_SEMAPHORE | 以信號量方式工作(每次讀取減 1) | ? 推薦 |
EFD_CLOEXEC | 執行 exec 時自動關閉描述符 | ? 推薦 |
EFD_NONBLOCK | 設置為非阻塞模式 | ? 視需求而定 |
EFD_SHARED_FCNTL_FLAGS | 允許在 fork 后共享文件鎖(Linux 4.7+) | ?? 較少用 |
0 (默認) | 默認行為(不帶任何標志) | ? 可用 |
詳解 (通俗解釋通俗易懂版本)
1?? EFD_SEMAPHORE
—— 類似信號量
效果:
啟用這個標志后,每次 read()
會把計數器減去 1,并返回 1。
🧠 比如:
你有一個糖果罐子,里面有 5 顆糖:
- 不加這個標志 → 第一個人來,把 5 顆都拿走了。
- 加了這個標志 → 每個人只能拿 1 顆,共 5 個人能拿到。
下面我們代碼演示下:
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE| EFD_NONBLOCK); // 啟用兩個標志uint64_t val;for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
效果如下:
- 這里我們往文件里寫了個5;也就可以理解成它的計數器被從一開始的0變成了5;而我們設置了semaphore模式,也就是每次計數器都會自動減一(一般如果設置了這個(在允許的條件下),此時每次read讀出來的都是1)。
- 然后我們又設置了非阻塞模式(nonblock),也就是不會阻塞,因此看到了上面的效果。
2?? EFD_CLOEXEC
—— 自動關閉(exec 時)
效果:
當你調用 exec()
(運行新程序)時,這個 eventfd 描述符會自動關閉,防止被新程序繼承。
🧠 比如:
你在執行一個新的程序,不想讓這個程序看到你之前的“糖果罐子”,那就加上這個標志。
efd = eventfd(0, EFD_CLOEXEC); // exec 時自動關閉
- 這里我們一般使用的時候默認加上就好。
3?? EFD_NONBLOCK
—— 非阻塞讀寫
效果:
設置為非阻塞模式后:
- 如果當前沒有數據可讀,
read()
不會等待,而是立即返回錯誤碼EAGAIN
,如果緩沖區滿了,write()
也不會等待,而是立即返回EAGAIN
。
🧠 比如:
你去看糖果罐子,如果里面沒糖了,你不等,直接離開。
演示下:
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(0, EFD_NONBLOCK);uint64_t val;
ssize_t s = read(efd, &val, sizeof(val));
if (s == -1 && errno == EAGAIN) {printf("現在沒有事件發生\n");
}return 0;
}
效果:
發現如果設置了:
- 直接返回-1,然后查看錯誤碼即可判斷是非阻塞模式。
如果沒設置:
- 發現一直阻塞住。
4?? EFD_SHARED_FCNTL_FLAGS(Linux 4.7+)
效果:
允許多個 fork
出來的子進程共享這個 eventfd
的文件鎖狀態(很少用)。
🧠 舉例理解:
多個小孩一起管理同一個糖果罐子,不會互相干擾。
?? 這個標志只在較新的 Linux 內核中支持,一般用戶不需要關心。
因此,這里就不演示,也不常用。
三.綜合測試體驗下
代碼如下:
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE|EFD_CLOEXEC| EFD_NONBLOCK); // 啟用兩個標志uint64_t val;for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
效果:
- 這里我們設置了非阻塞,因此最后會看到
NO more events
,其次就是計數器寫成5,每次讀取都減1,然后每次讀出的都是1,當最后一次減完0了,然后是非阻塞因此會這樣。
如果我們寫入的值和eventfd本身初始化的值不同呢(他就會按照寫入的來初始化計數器了):
代碼如下:
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE|EFD_CLOEXEC| EFD_NONBLOCK); // 啟用兩個標志uint64_t val=10;ssize_t s = write(efd, &val, sizeof(val));for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
- 因此當它讀完5個1后還會繼續讀,直到完成10個:
效果:
如果不設置EFD_SEMAPHORE
呢?
代碼如下:
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main()
{int efd = eventfd(5, EFD_CLOEXEC | EFD_NONBLOCK); // 啟用兩個標志uint64_t val;for (int i = 0; i < 6; ++i){ssize_t s = read(efd, &val, sizeof(val));if (s == -1){if (errno == EAGAIN | EWOULDBLOCK)printf("No more events.\n");elseperror("read");}else{printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
- 此時它就會一次性都讀出來,然后計數器瞬間清零。
效果:
四.相關問題及使用技巧
相關問題:
問題 | 回答 |
---|---|
eventfd 是不是只能用于線程間通信? | 不是,也可以用于父子進程之間通信 |
eventfd 能不能和 epoll 一起用? | 當然可以!這是最常見用法之一 |
eventfd 和 pipe 有什么區別? | eventfd 更輕量,適合簡單通知;pipe 適合傳輸大量數據 |
eventfd 的最大值是多少? | 最大值是 0xFFFFFFFFFFFFFFFE(接近 18e18) |
eventfd 會不會導致內存泄漏? | 不會,只要記得 close(efd) 就行 |
使用技巧:(個人看法)
一般我們可以默認把EFD_CLOEXEC
加上;然后對于需求來決定是否加上EFD_NONBLOCK
;對于EFD_SEMAPHORE
也就是想讓一次讀完還是多次也是根據自己需求來完成的(常用的也就是這三個
)。
五.簡單基于eventfd
與epoll
多線程通知測試
大致測流程:
- 初始化 eventfd。
- 初始化 epoll。
- 將 eventfd 注冊到 epoll。
- 啟動多個線程調用 epoll_wait 等待事件。
- 主線程寫入 eventfd 觸發事件。
- 所有監聽線程收到事件并處理。
看圖:
源碼:
#include <iostream>
#include <thread>
#include <vector>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <cstring>
#include <cstdint>
#include <functional>
#include <chrono>// 線程數量
const int THREAD_COUNT = 3;int main() {// 1. 創建 eventfd (初始值為0)int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);if (efd == -1) {perror("eventfd");return 1;}// 2. 創建 epoll 實例int epfd = epoll_create1(0);if (epfd == -1) {perror("epoll_create1");close(efd);return 1;}// 3. 將 eventfd 添加進 epollstruct epoll_event ev;ev.events = EPOLLIN; // 只關心可讀事件ev.data.fd = efd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev) == -1) {perror("epoll_ctl: add");close(epfd);close(efd);return 1;}// 4. 創建線程池,每個線程調用 epoll_wait 等待事件std::vector<std::thread> threads;for (int i = 0; i < THREAD_COUNT; ++i) {threads.emplace_back([=]() {std::cout << "Thread [" << std::this_thread::get_id() << "] is waiting for event..." << std::endl;struct epoll_event events[10];while (true) {int n = epoll_wait(epfd, events, 10, -1); // 永遠等待if (n == -1) {perror("epoll_wait error");break;}for (int j = 0; j < n; ++j) {if (events[j].data.fd == efd && (events[j].events & EPOLLIN)) {uint64_t u;ssize_t s = read(efd, &u, sizeof(uint64_t));if (s != sizeof(uint64_t)) {perror("read eventfd");continue;}std::cout << "Thread [" << std::this_thread::get_id()<< "] received event, count: " << u << std::endl;}}}});}// 5. 主線程休眠一段時間后發送事件std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "Main thread is sending event to all workers..." << std::endl;uint64_t u = 1;if (write(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("write eventfd");}// 6. 等待所有線程結束(這里為了簡單,實際應優雅退出)std::this_thread::sleep_for(std::chrono::seconds(2)); // 給子線程足夠時間響應for (auto& t : threads) {if (t.joinable()) {t.detach(); // 或者 join()}}// 7. 清理資源close(epfd);close(efd);return 0;
}
先看現象:
解釋下:
- 首先搞三線程,然后往epoll模型的監測fd中加入efd,三個線程都進行監測;如果主線程往efd中寫入了1,那么就只會被一個線程讀取然后打印出來,其他線程都在epoll這里阻塞;最后全部線程都被終止即結束。
六. 小白總結
下面是博主總結的一張使用圖:
通俗總結:
eventfd 就像一個“計數器盒子”,你可以往里放數字,也可以往外取。通過設置不同的標志(flag),你可以控制它是“一次全拿走”還是“每次拿一個”,還可以讓它“不阻塞”、“自動關閉”等等。掌握這些標志,就能靈活運用它來做多線程同步、異步通知等高級功能!