一个由poll引发的思考

晚上试了一波poll,写了一段巨烂的代码。

不过这段代码,让我猜到了printf和STDOUT_FILENO的关系,还有他缓冲的机制。

#include<stdio.h>
#include<unistd.h>
#include<poll.h>

#define TIMEOUT 5

char cReadBuffer[1024] = {'\0'};
int main(void)
{
    struct pollfd fds[2];
    int ret;
    int iReadLen;
    cReadBuffer[0] ='\0';

    while(1)
    {
        fds[0].events = POLLIN;
        fds[0].fd = STDIN_FILENO;

        fds[1].events = POLLOUT;
        fds[1].fd = STDOUT_FILENO;

        ret = poll(fds,2,TIMEOUT*1000);
        if (-1 == ret)
        {
            perror("poll");
            return 1;
        }

        if(!ret)
        {
            printf("%d seconds elapsed.\n",TIMEOUT);

            return 0;
        }

        if(fds[0].revents & POLLIN)
        {
            iReadLen = read(STDIN_FILENO,cReadBuffer,sizeof(cReadBuffer));
            if(-1 != iReadLen)
            {
                printf("read from STDIN:%s.",cReadBuffer);
            }
        }

        if(fds[1].revents & POLLOUT)
        {
            if('\0' != cReadBuffer[0])
            {
                if(-1 == write(STDOUT_FILENO,cReadBuffer,sizeof(cReadBuffer)) )
                {
                    perror("write");
                }
                cReadBuffer[0] = '\0';
            }
        }
    }
    return 0;
}

想了很久,终于搞懂为啥是这么显示的。


<think>好的,我现在要回答用户关于I/O通知机制中的水平触发(Level-Triggered,LT)和边缘触发(Edge-Triggered,ET)的问题。用户之前已经询问过信号通知机制,现在想更深入了解这两种触发方式。我需要确保回答结构清晰,符合用户给出的格式要求,同时内容准确可靠。 首先,我需要回忆水平触发和边缘触发的基本概念。水平触发指的是当文件描述符处于就绪状态时,会持续通知应用程序,直到状态被处理。而边缘触发只在状态变化时通知一次,无论之后是否仍然就绪。这两者在多路复用机制如epoll中很常见。 接下来,用户希望对比这两种机制,包括它们的定义、工作模式、典型实现、使用场景、优缺点以及注意事项。我需要确保每个部分都有清晰的对比,并举例说明,比如使用epoll的情况。同时,要符合LaTeX格式要求,行内公式用$...$,独立公式用$$...$$,但根据用户的问题,可能这里公式使用较少,更多的是技术术语,比如函数名或系统调用,可能需要用代码块展示。 需要注意的是,用户可能对这两种机制的实际应用和选择依据感兴趣,所以需要强调在不同场景下的选择建议。比如,水平触发适合简单逻辑,边缘触发适合高性能需求,但需要处理未读完的数据。 然后,检查是否有遗漏的关键点。例如,边缘触发需要非阻塞IO,循环读取直到EAGAIN错误,而水平触发则可以在任何时间处理,不需要立即处理所有数据。这涉及到使用时需要注意的事项,比如在边缘触发下如果不完全读取数据,可能会丢失后续事件,导致数据滞留。 另外,用户可能想知道为什么边缘触发更高效,因为它减少了系统调用的次数,但同时也增加了实现的复杂性。这部分需要明确说明,避免混淆。 还需要考虑用户可能的实际应用场景,比如网络服务器如何处理高并发连接,选择哪种触发方式更合适。比如Nginx默认使用边缘触发,而一些传统的服务器可能使用水平触发。 最后,确保回答符合用户提供的系统级指令,特别是中文回答,结构清晰,分点明确,使用代码块示例,比如epoll的边沿触发设置代码。同时注意不要使用Markdown格式,但用户提供的例子中使用了代码块和表格,这可能符合用户的可接受格式,但需要确认是否在用户可见层允许。根据用户之前的回复,似乎允许代码块和表格,所以可以继续使用。 总结思考步骤: 1. 定义两种触发机制,明确区别。 2. 分别详细说明工作模式,优缺点。 3. 典型实现举例,如epoll。 4. 使用场景对比,举例说明。 5. 注意事项,特别是边缘触发的非阻塞读取。 6. 给出选择建议,帮助用户根据需求做决策。 现在开始组织内容,确保每个部分清晰,对比明显,并给出实际示例和代码片段,以增强理解。</think>### I/O 多路复用中的通知机制:水平触发 vs. 边缘触发 --- #### 一、核心概念对比 | **特性** | **水平触发(Level-Triggered, LT)** | **边缘触发(Edge-Triggered, ET)** | |------------------|-------------------------------------------------------|------------------------------------------------| | **通知时机** | 当文件描述符(fd)**处于就绪状态时持续通知** | 仅当 fd 状态**发生改变时通知一次**(如从不可读变为可读) | | **数据未处理后果** | 内核会**反复通知**,直到数据被完全处理 | 内核**不再通知**,可能遗漏未处理的数据 | | **典型实现** | `select()`, `poll()`, `epoll`(默认 LT 模式) | `epoll`(需显式设置 `EPOLLET` 标志) | | **编程复杂度** | 较低(允许延迟处理数据) | 较高(需确保一次性处理所有数据) | --- #### 二、工作机制详解 ##### 1. 水平触发(LT) - **工作模式** 当 fd **保持就绪状态时**,内核会**持续通知**应用程序。例如: - 若 socket 接收缓冲区有 2KB 数据未被读取,每次调用 `epoll_wait()` 都会报告该 fd 可读。 - 应用程序可以分多次读取(如每次读 1KB),未读完时内核仍会提醒。 - **代码示例**(`epoll` 默认 LT 模式) ```c struct epoll_event event; event.events = EPOLLIN; // 默认 LT 模式 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event); ``` - **优点** - 容错性高:未及时处理数据不会导致事件丢失 - 编程简单:无需一次性处理所有数据 - **缺点** - 频繁唤醒:若大量 fd 长期就绪,可能引发无效的系统调用 ##### 2. 边缘触发(ET) - **工作模式** 仅当 fd 状态**从非就绪变为就绪时通知一次**。例如: - 若 socket 接收缓冲区从空变为有 2KB 数据,内核发送一次通知。 - 应用程序必须**一次性读取所有数据**(直到返回 `EAGAIN` 错误),否则剩余数据将不再触发通知。 - **代码示例**(显式启用 ET 模式) ```c struct epoll_event event; event.events = EPOLLIN | EPOLLET; // 启用 ET 模式 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event); ``` - **优点** - 减少无效唤醒:通知次数与状态变化次数严格一致 - 性能更高:适合高并发场景(如百万级连接) - **缺点** - 必须使用**非阻塞 I/O** 并循环读取数据,直到返回错误 - 编程复杂:需处理数据未读尽导致的“饥饿”问题 --- #### 三、关键差异场景演示 假设 socket 接收缓冲区先后到达 2KB 和 3KB 数据: 1. **水平触发场景** - 第一次 `epoll_wait()` 返回可读 → 读取 1KB - 第二次 `epoll_wait()` **仍返回可读** → 继续读取剩余 1KB - 第三次 `epoll_wait()` 阻塞(缓冲区已空) - 新数据 3KB 到达 → 第四次 `epoll_wait()` 返回可读 2. **边缘触发场景** - 第一次 `epoll_wait()` 返回可读 → **必须一次性读取全部 2KB**(否则丢失后续通知) - 新数据 3KB 到达 → 触发第二次 `epoll_wait()` 通知 - 必须再次循环读取直到 `recv()` 返回 `EAGAIN` --- #### 四、使用注意事项 ##### 通用规则 - **边缘触发必须配合非阻塞 I/O** ```c // 设置 fd 为非阻塞模式 int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); ``` - **ET 模式下的标准读取逻辑** ```c while (1) { ssize_t cnt = read(fd, buf, sizeof(buf)); if (cnt == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 数据已读完 else handle_error(); } else if (cnt == 0) { /* 连接关闭 */ } else process_data(buf, cnt); } ``` ##### 选择建议 | **场景** | **推荐模式** | **原因** | |------------------------------|---------------|--------------------------------------------------------------------------| | 简单应用、低并发 | 水平触发 | 避免复杂的状态管理 | | 高频事件(如实时游戏服务器) | 边缘触发 | 减少无效的系统调用,提升吞吐量 | | 文件 I/O | 水平触发 | 文件始终“可读”(EOF前),ET 模式会导致无限循环 | | 需要兼容旧代码 | 水平触发 | `select`/`poll` 仅支持 LT | --- #### 五、性能与设计权衡 - **LT 的潜在问题** $$ \text{无效唤醒次数} \propto \text{就绪 fd 数量} \times \text{检查频率} $$ 当大量 fd 长期就绪时,LT 会导致频繁的无效返回(如监听的 socket 长期有连接请求)。 - **ET 的优化本质** $$ \text{通知次数} = \text{状态变化次数} $$ 通过减少唤醒次数降低 CPU 占用,但要求应用程序更积极地处理数据。 --- #### 六、总结 - **水平触发**: **“有数据时提醒你”** → 适合对延迟不敏感、逻辑简单的场景。 - 优点:容错性强,编程简单 - 缺点:可能产生冗余系统调用 - **边缘触发**: **“数据到达时提醒一次”** → 适合高并发、低延迟的关键系统。 - 优点:最大化吞吐量,减少内核-用户态切换 - 缺点:需严格处理数据边界,编程复杂度高 **实际应用建议**: - Web 服务器(如 Nginx)默认使用 ET 模式,通过非阻塞 I/O + 状态机实现高性能。 - 嵌入式或低负载服务可优先选择 LT 模式以简化开发。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值