Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
---

<!-- COVERAGE_START -->
![English Coverage](https://img.shields.io/badge/en_coverage-100%25-green.svg) 494/494 docs translated
![English Coverage](https://img.shields.io/badge/en_coverage-99%25-green.svg) 494/500 docs translated
<!-- COVERAGE_END -->

## 这是什么项目
Expand Down
40 changes: 40 additions & 0 deletions code/volumn_codes/vol8-labs/lab0-mini-reactor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.14)
project(lab0-mini-reactor LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Catch2 v3(FetchContent 拉取;首次配置需联网)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.0)
FetchContent_MakeAvailable(Catch2)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)

# ---------- 普通版:reactor 库(参考实现)+ 测试 ----------
add_library(reactor STATIC src/reactor.cpp)
target_include_directories(reactor PUBLIC include)
target_compile_options(reactor PUBLIC -Wall -Wextra -Wpedantic -g)

add_executable(lab0_tests tests/lab0_tests.cpp)
target_link_libraries(lab0_tests PRIVATE reactor Catch2::Catch2WithMain)
target_compile_options(lab0_tests PRIVATE -Wall -Wextra -Wpedantic -g -pthread)
target_link_options(lab0_tests PRIVATE -pthread)

# ---------- TSan 版:同样的源码,编译期开 -fsanitize=thread ----------
# reactor 库也单独编一份 tsan 版,否则它内部的并发(如 stop_ 原子)在 TSan 下不可见
add_library(reactor_tsan STATIC src/reactor.cpp)
target_include_directories(reactor_tsan PUBLIC include)
target_compile_options(reactor_tsan PUBLIC -Wall -Wextra -Wpedantic -g -fsanitize=thread -pthread)
target_link_options(reactor_tsan PUBLIC -fsanitize=thread)

add_executable(lab0_tests_tsan tests/lab0_tests.cpp)
target_link_libraries(lab0_tests_tsan PRIVATE reactor_tsan Catch2::Catch2WithMain)
target_compile_options(lab0_tests_tsan PRIVATE -Wall -Wextra -Wpedantic -g -fsanitize=thread -pthread)
target_link_options(lab0_tests_tsan PRIVATE -fsanitize=thread)

# 便捷:make check = 跑普通测试
add_custom_target(check COMMAND $<TARGET_FILE:lab0_tests>)
47 changes: 47 additions & 0 deletions code/volumn_codes/vol8-labs/lab0-mini-reactor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Lab 0 · mini Reactor echo server

配套文章:`documents/vol8-domains/networking/lab0-mini-reactor.md`

在 epoll 上实现一个最小的 Reactor(事件循环 + handler 注册表),做成能扛并发的 echo server。MS1-4 四个 milestone,每个的验收都是**对抗性的**(并发不崩、ET 大 burst 不丢数据、stop 不挂、TSan 无 race)。

## 怎么做这个 Lab

1. 读 `include/net/reactor.hpp`(接口)。
2. 把 `src/reactor.cpp` 清空(现在是**参考答案**,留着对照思路),对照接口自己写一遍 `Reactor`。
3. 构建 + 跑测试,让 `tests/lab0_tests.cpp` 的 4 个用例(MS1-4)逐个变绿。

## 构建

```bash
cd code/volumn_codes/vol8-labs/lab0-mini-reactor
cmake -S . -B build
cmake --build build -j
```

首次配置会从 github 拉 Catch2(FetchContent)。**若网络受限拉不下来**,手动浅克隆一份、用 `-DFETCHCONTENT_SOURCE_DIR_CATCH2` 指过去:

```bash
git clone --depth 1 -b v3.5.0 https://github.com/catchorg/Catch2.git /tmp/catch2
cmake -S . -B build -DFETCHCONTENT_SOURCE_DIR_CATCH2=/tmp/catch2
cmake --build build -j
```

## 跑测试

```bash
./build/lab0_tests # 普通测试:MS1-4
./build/lab0_tests_tsan # TSan 版:同样的用例,编译期开 -fsanitize=thread
```

预期:`All tests passed (6 assertions in 4 test cases)`,且 TSan 版**无任何 warning**。

## MS1-4 验收速查

| MS | 验收(对抗性) | 抓的坑 |
|----|------|------|
| MS1 | 单连接 + 顺序多连接 echo 全对 | 事件循环转起来 |
| MS2 | 16 并发 echo 全对,**TSan 无 race** | handler 注册表 / 隐藏 data race |
| MS3 | ET + 100KB burst,echo **正好 100000** 字节 | ET 不循环读到 EAGAIN → 丢数据([02 篇](../../../../documents/vol8-domains/networking/02-epoll-io-multiplexing.md)那个坑) |
| MS4 | `stop()` 后 `run()` 2s 内返回 | epoll_wait 阻塞、关闭挂死 |

参考实现(`src/reactor.cpp`)一个值得注意的点:`run()` 调 handler 前先 `Handler h = it->second;` 拷一份——因为 handler 在 EOF 时会 `remove` 自己,直接调 map 里那个正在执行的 `std::function` 是 use-after-free(TSan 会抓)。这是 reactor 的经典自删除坑。
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <functional>
#include <unordered_map>

namespace net {

/// 事件处理器:某个 fd 就绪时被调用,传入就绪的事件位(EPOLLIN/EPOLLOUT/...)。
using Handler = std::function<void(std::uint32_t events)>;

/// 一个最小的 Reactor:epoll 事件循环 + fd→handler 注册表。
///
/// 所有 handler 都在 run() 的线程上同步执行(单线程 Reactor)——这是它"免锁"
/// 的关键:同一时刻只有一个 handler 在跑,共享状态不会被并发改。多线程安全靠
/// stop()(用 eventfd 唤醒阻塞中的 epoll_wait)。
///
/// 配套 documents/vol8-domains/networking/lab0-mini-reactor.md。
class Reactor {
public:
Reactor();
~Reactor();

Reactor(const Reactor&) = delete;
Reactor& operator=(const Reactor&) = delete;

/// 注册 fd:关心 events,就绪时调 h。fd 必须是有效的、且尚未注册。
void add(int fd, std::uint32_t events, Handler h);

/// 修改已注册 fd 关心的事件(如 LT→ET、加 EPOLLOUT)。
void modify(int fd, std::uint32_t events);

/// 注销 fd(从兴趣表移除 + 删 handler)。
void remove(int fd);

/// 运行事件循环,阻塞直到 stop() 被调用。
void run();

/// 请求停止(可从别的线程或信号 handler 调用;eventfd 写唤醒 epoll_wait)。
void stop();

private:
int ep_fd_{-1};
int stop_fd_{-1}; // eventfd,用于唤醒
std::atomic<bool> stop_{false};
std::unordered_map<int, Handler> handlers_;
};

} // namespace net
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once
#include <unistd.h>

namespace net {

/// RAII 包装一个 POSIX fd:不可拷贝、仅可移动,析构时 close。
/// (与 01-modern-socket 同一个,Lab 里复用。)
class UniqueFd {
public:
UniqueFd() = default;
explicit UniqueFd(int fd) : fd_{fd} {}
~UniqueFd() { reset(); }

UniqueFd(const UniqueFd&) = delete;
UniqueFd& operator=(const UniqueFd&) = delete;

UniqueFd(UniqueFd&& other) noexcept : fd_{other.fd_} { other.fd_ = -1; }
UniqueFd& operator=(UniqueFd&& other) noexcept {
if (this != &other) {
reset();
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}

void reset() {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
}
int get() const { return fd_; }
int release() {
int f = fd_;
fd_ = -1;
return f;
}
explicit operator bool() const { return fd_ >= 0; }

private:
int fd_{-1};
};

} // namespace net
84 changes: 84 additions & 0 deletions code/volumn_codes/vol8-labs/lab0-mini-reactor/src/reactor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Reactor 参考实现(答案版)。Lab 的任务是:读完 reactor.hpp 这个接口,
// 自己在 src/ 里重写一遍,让 tests/ 全绿。这个文件是给你对照的参考答案。
#include "net/reactor.hpp"

#include <array>
#include <cerrno>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <unistd.h>

namespace net {

Reactor::Reactor() {
ep_fd_ = ::epoll_create1(0);
stop_fd_ = ::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
// 把 stop_fd_ 也挂进 epoll,这样 stop() 往它写一字节就能唤醒阻塞中的 epoll_wait
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = stop_fd_;
::epoll_ctl(ep_fd_, EPOLL_CTL_ADD, stop_fd_, &ev);
}

Reactor::~Reactor() {
if (ep_fd_ >= 0)
::close(ep_fd_);
if (stop_fd_ >= 0)
::close(stop_fd_);
}

void Reactor::add(int fd, std::uint32_t events, Handler h) {
handlers_[fd] = std::move(h);
epoll_event ev{};
ev.events = events;
ev.data.fd = fd;
::epoll_ctl(ep_fd_, EPOLL_CTL_ADD, fd, &ev);
}

void Reactor::modify(int fd, std::uint32_t events) {
epoll_event ev{};
ev.events = events;
ev.data.fd = fd;
::epoll_ctl(ep_fd_, EPOLL_CTL_MOD, fd, &ev);
}

void Reactor::remove(int fd) {
handlers_.erase(fd);
::epoll_ctl(ep_fd_, EPOLL_CTL_DEL, fd, nullptr);
}

void Reactor::stop() {
stop_.store(true, std::memory_order_relaxed);
std::uint64_t one = 1;
::write(stop_fd_, &one, sizeof(one)); // 唤醒 epoll_wait
}

void Reactor::run() {
std::array<epoll_event, 64> events;
while (!stop_.load(std::memory_order_relaxed)) {
int n = ::epoll_wait(ep_fd_, events.data(), static_cast<int>(events.size()), -1);
if (n < 0) {
if (errno == EINTR)
continue; // 被信号打断,重试
break; // 真出错,退出
}
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
if (fd == stop_fd_) {
std::uint64_t v; // 清掉 eventfd 的计数
::read(stop_fd_, &v, sizeof(v));
continue; // stop_ 会在 while 条件里被检查
}
auto it = handlers_.find(fd);
if (it != handlers_.end()) {
// 拷一份 handler 再调:handler 里若 remove 自己(EOF 时常见),
// 会从 handlers_ 擦除正在执行的这个 std::function,直接调 it->second
// 就是 use-after-free。先拷一份,本次调用就安全了。
Handler h = it->second;
h(events[i].events);
}
}
}
}

} // namespace net
Loading
Loading