# 第 33 章实验:用 select() 写事件循环 对应页面:`chapters/33-events.html`。本目录有三个小程序: | 文件 | 做什么 | |---|---| | `echo_server.c` | 单线程、基于 `select()` 的多客户端 TCP echo 服务器(只监听 127.0.0.1)。`-b 毫秒` 让每个事件处理器里调用一次阻塞的 `usleep()`,模拟"在事件处理器里做了阻塞 I/O"。 | | `client.c` | echo 客户端:连接服务器,发送 N 条消息,测量每条消息的往返时间(RTT)。 | | `pipes.c` | 父进程 fork 出 3 个子进程,它们以 150/250/400 ms 的间隔往各自的管道写消息;父进程用 `select()` 同时监听 3 个管道读端,按"谁先就绪先处理谁"的顺序读出。 | | `run_demo.sh` | 自动化演示:先以 `-b 0` 启动服务器并同时跑 3 个客户端,再以 `-b 100` 重跑一遍做对照;每轮结束都会 kill 掉服务器。 | ## 怎么跑 ```bash cd labs/33-events make # gcc -Wall -Wextra 编译,应无警告 ./pipes # 多管道 select 演示 ./run_demo.sh 9090 # echo 服务器 + 3 个并发客户端,两轮对照(端口可换) ``` 也可以手动玩: ```bash ./echo_server -p 9090 & # 后台启动 ./client A -n 5 & ./client B -n 5 & wait # 或者直接用 bash 的 /dev/tcp: exec 3<>/dev/tcp/127.0.0.1/9090; echo hi >&3; head -c 3 <&3; exec 3>&- kill %1 # 用完记得关掉服务器 ``` ## 该观察到什么 1. **pipes**:父进程只有一个线程,却能"同时"等 3 个管道。输出的时间戳大约是 150、250、300、400、450、500、600… ms, 正好是三个子进程写入时间的归并顺序——`select()` 告诉父进程哪个 fd 就绪,父进程就处理哪个。子进程关闭写端后, 读端变为"可读"并且 `read()` 返回 0(EOF),父进程把它从集合中去掉。 2. **echo_server -b 0**:3 个客户端的平均 RTT 都在 1 ms 以下。日志里能看到 `select` 每轮返回就绪 fd 的列表, 监听 socket 就绪时 `accept()`,客户端 socket 就绪时 `read()` 并回显——没有任何锁。 3. **echo_server -b 100**:每条消息只让处理器"阻塞" 100 ms,但**所有**客户端的 RTT 都被拖到 100~400 ms: 处理 A 的消息时,B、C 的消息已经到了,却只能在内核缓冲区里排队,因为唯一的事件循环卡在 `usleep()` 里。 这就是书中说的"事件服务器里绝不能调用阻塞调用"(TIP: DON'T BLOCK IN EVENT-BASED SERVERS)。 如果换成每连接一个线程,一个线程睡眠不会影响其他线程。 ## 为什么 - `select()` 的输入是一个 fd 位图(我们关心哪些 fd),返回时内核把它**改写**为"已就绪的子集",返回值 `rc` 是就绪 fd 的个数。 所以每轮循环都必须重新构造(或复制)一份集合——`echo_server.c` 用 `fd_set readFDs = all;` 做这件事。 - 事件循环里"下一个处理哪个 fd"是程序自己决定的,这就是事件驱动方式对调度的显式控制。 - 单线程处理所有事件,所以处理器之间不会被抢占打断,也就不需要锁;代价是任何一个处理器都不能阻塞。