Một thread worker vừa đẩy công việc vào queue, nhưng event-loop thread đang ngủ trong epoll_wait(). Polling queue liên tục tốn CPU; condition variable lại không thể đưa trực tiếp vào epoll. eventfd() giải quyết đúng khoảng trống này bằng một counter 64 bit trong kernel, có thể được ghi từ producer và đọc như một file descriptor ở consumer.
Bài viết xây dựng mẫu queue + eventfd an toàn, giải thích counter mode và semaphore mode, quy tắc đọc/ghi 8 byte, xử lý edge-triggered epoll, overflow và shutdown. Nội dung dựa trên Linux man-pages về eventfd(2), epoll(7), read(2) và write(2).
1. eventfd là counter thông báo, không phải queue dữ liệu
eventfd() tạo một object chứa counter unsigned 64 bit và trả một file descriptor. write() cộng giá trị vào counter; khi counter lớn hơn 0, FD readable với poll/epoll. read() tiêu thụ counter theo mode đã chọn.
Counter chỉ nói “có sự kiện/công việc”. Payload thật nên nằm trong một queue được đồng bộ bằng mutex hoặc atomics. Tách hai vai trò giúp event loop được đánh thức hiệu quả mà không ép mọi dữ liệu đi qua pipe.
eventfd không thay thế tính đúng đắn của queue. Mutex, memory ordering, ownership và lifetime của work item vẫn phải được thiết kế độc lập.
2. Tạo eventfd cho epoll
#include <sys/eventfd.h>
int wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (wake_fd == -1)
fail_errno("eventfd");
- Giá trị đầu 0 nghĩa counter chưa có notification.
EFD_NONBLOCKlàm read/write trảEAGAINthay vì chặn.EFD_CLOEXECngăn descriptor vô tình sống quaexecve().
Không cần EFD_SEMAPHORE cho mẫu “đánh thức loop rồi drain queue”. Counter mode mặc định coalesce nhiều lần wake thành một số.
3. Semantics đọc và ghi chính xác
| Thao tác | Counter mode | EFD_SEMAPHORE |
|---|---|---|
write(uint64_t n) | Cộng n vào counter; n phải khác UINT64_MAX. | |
read() khi counter > 0 | Trả toàn bộ counter rồi reset về 0. | Trả 1 và giảm counter đi 1. |
| Counter = 0 | Block hoặc trả EAGAIN nếu nonblocking. | |
Cả read và write phải truyền buffer ít nhất 8 byte; nhỏ hơn sẽ nhận EINVAL. Giá trị dùng native byte order vì đây là kernel ABI trên cùng host, không phải giao thức mạng.
4. Producer: enqueue trước, wake sau
bool submit(struct work_item *item)
{
pthread_mutex_lock(&queue_mutex);
bool ok = queue_push(&work_queue, item);
pthread_mutex_unlock(&queue_mutex);
if (!ok)
return false;
uint64_t one = 1;
for (;;) {
ssize_t n = write(wake_fd, &one, sizeof(one));
if (n == (ssize_t)sizeof(one))
return true;
if (n == -1 && errno == EINTR)
continue;
if (n == -1 && errno == EAGAIN) {
/* Counter đã gần trần: FD vốn đã readable. */
record_wakeup_saturation();
return true;
}
return handle_wakeup_failure(item, errno);
}
}
Thứ tự bắt buộc là publish item vào queue trước rồi mới ghi wake. Nếu đảo lại, consumer có thể thức dậy, thấy queue rỗng, quay lại ngủ trước khi item xuất hiện.
Ví dụ dùng mutex tạo quan hệ đồng bộ rõ ràng. Với lock-free queue, phải dùng memory ordering đúng; syscall eventfd không tự sửa một thuật toán queue sai.
5. Consumer: đọc counter rồi drain queue
static void handle_wakeup(int wake_fd)
{
uint64_t notifications = 0;
for (;;) {
uint64_t value;
ssize_t n = read(wake_fd, &value, sizeof(value));
if (n == (ssize_t)sizeof(value)) {
notifications += value;
continue;
}
if (n == -1 && errno == EINTR)
continue;
if (n == -1 && errno == EAGAIN)
break;
fail_errno("read eventfd");
}
size_t processed = 0;
while (processed < WORK_BUDGET) {
struct work_item *item = queue_pop_synchronized(&work_queue);
if (item == NULL)
break;
process_item(item);
processed++;
}
record_wakeup_batch(notifications, processed);
if (!queue_is_empty_synchronized(&work_queue))
schedule_local_continuation();
}
notifications không nhất thiết bằng số item: producer có thể batch nhiều item cho một wake, item có thể bị hủy, hoặc nhiều write được coalesce. Queue mới là source of truth; counter là tín hiệu và metric batching.
6. Kết hợp EPOLLET mà không mất wakeup
struct epoll_event ev = {
.events = EPOLLIN | EPOLLET,
.data.fd = wake_fd
};
if (epoll_ctl(epfd, EPOLL_CTL_ADD, wake_fd, &ev) == -1)
fail_errno("epoll_ctl eventfd");
Với edge-triggered epoll, đọc đến EAGAIN. Nếu chỉ đọc một phần trong semaphore mode, counter vẫn dương nhưng có thể không xuất hiện edge mới, khiến consumer ngủ dù token còn lại.
Counter mode thường chỉ cần một read thành công để về 0, nhưng loop đến EAGAIN vẫn là mẫu chắc chắn khi producer có thể ghi đồng thời. Level-triggered đơn giản hơn và cũng hoàn toàn hợp lệ nếu workload không cần EPOLLET.
7. EFD_SEMAPHORE dùng khi nào?
Với EFD_SEMAPHORE, mỗi read trả 1 và giảm counter một đơn vị. Mode này hữu ích khi counter đại diện token tài nguyên và mỗi consumer phải claim đúng một token. Nó không tự cung cấp fairness, payload, priority hay cancellation.
int token_fd = eventfd(initial_tokens,
EFD_SEMAPHORE | EFD_NONBLOCK | EFD_CLOEXEC);
Đừng bật semaphore mode chỉ vì có nhiều item trong queue. Việc phải đọc một lần trên mỗi item làm mất lợi ích batching và có thể gây starvation cho FD khác nếu drain không có budget.
8. Overflow và backpressure
Counter lớn nhất có thể lưu từ write thông thường là UINT64_MAX - 1. Ghi một giá trị khiến vượt trần sẽ block hoặc trả EAGAIN ở nonblocking mode; ghi chính UINT64_MAX trả EINVAL.
Với write 1 và consumer khỏe, chạm trần gần như là dấu hiệu lỗi lifecycle nghiêm trọng. Tuy vậy queue có thể đầy rất lâu trước counter. Backpressure phải đặt trên số item/bytes của queue:
- queue bounded với kết quả submit rõ ràng;
- quota theo producer/tenant;
- drop/coalesce cho công việc thay thế được;
- deadline và cancellation cho item cũ;
- metric queue depth, oldest age và rejection.
9. Wake coalescing để giảm syscall
Mỗi producer ghi eventfd cho mỗi item là đúng nhưng có thể nhiều syscall. Một tối ưu phổ biến là chỉ wake khi queue chuyển từ empty sang non-empty:
- producer push item dưới cùng cơ chế đồng bộ;
- nếu trước đó queue rỗng, write 1;
- consumer drain theo budget;
- nếu còn item, tự lên lịch continuation thay vì ngủ.
Protocol này dễ có lost wake nếu kiểm tra “rỗng” và chuyển trạng thái ngủ không nguyên tử theo cùng state machine. Chỉ áp dụng sau khi có stress test race; mẫu wake mỗi batch đơn giản thường đáng tin hơn tối ưu sớm.
10. Shutdown và lifetime
Không để producer ghi vào FD đã đóng hoặc bị hệ điều hành tái sử dụng cho tài nguyên khác. Quy trình shutdown nên:
- đánh dấu không nhận submit mới;
- chờ producer đang submit rời critical section;
- wake loop để nó quan sát trạng thái shutdown;
- drain hoặc cancel queue theo policy;
- gỡ FD khỏi epoll, đóng trong owner thread;
- join threads và giải phóng queue.
Sau fork, descriptor ở parent và child trỏ cùng eventfd object. Dùng EFD_CLOEXEC và đóng bản không cần thiết; nếu hai process cần kênh độc lập, tạo eventfd riêng.
11. eventfd, pipe hay condition variable?
| Công cụ | Điểm mạnh | Giới hạn |
|---|---|---|
| eventfd | Một FD, counter, tích hợp epoll, overhead thấp. | Không mang payload tùy ý. |
| pipe/socketpair | Mang byte/message nhỏ, portable hơn. | Hai FD, buffer và framing phức tạp hơn. |
| condition variable | Tự nhiên cho thread pool thuần túy. | Không trực tiếp nằm trong epoll. |
| futex | Primitive thấp, fast path user-space. | Khó dùng đúng, không phải FD readiness thông thường. |
Chọn eventfd khi nhu cầu chính là đánh thức một FD-based event loop; chọn pipe/socketpair khi notification cần mang dữ liệu nhỏ trực tiếp hoặc cần portability ngoài Linux.
Checklist production
- Tạo với
EFD_NONBLOCK | EFD_CLOEXEC. - Queue là source of truth; eventfd chỉ là wake hint/counter.
- Producer publish item trước rồi mới write wake.
- Read/write dùng đúng 8 byte và xử lý
EINTR,EAGAIN. - EPOLLET được drain đến
EAGAIN. - Không giả định counter bằng queue length.
- Queue có bound và policy backpressure độc lập.
- Consumer có work budget để không làm đói socket/timer khác.
- Shutdown ngăn submit trước khi đóng FD.
- Stress test producer đồng thời, queue transition và shutdown race.
Kết luận
eventfd() là primitive nhỏ nhưng rất hợp với kiến trúc Linux hướng sự kiện: nhiều producer có thể đánh thức một epoll owner bằng một FD và kernel tự coalesce notification qua counter. Mẫu bền vững là queue giữ payload, eventfd chỉ báo có việc, producer enqueue trước khi wake, consumer drain có budget và lifecycle được sở hữu rõ ràng. Khi tách đúng các trách nhiệm này, hệ thống vừa giảm polling vừa tránh biến counter thành một queue giả dễ race.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.