Linux阻塞与非阻塞I/O:从原理到实践详解

news2025/4/26 12:59:17

Linux阻塞与非阻塞I/O:从原理到实践详解

1. 阻塞与非阻塞I/O基础概念

1.1 阻塞与非阻塞简介

在Linux系统编程中,I/O操作可以分为两种基本模式:阻塞I/O非阻塞I/O。这两种模式决定了当设备或资源不可用时,程序的行为方式。

阻塞I/O就像你在餐厅点餐后坐在座位上等待服务员上菜。在此期间你不能做其他事情,只能等待食物送到面前。在编程中,这意味着当程序执行I/O操作时,如果数据未准备好,进程会进入睡眠状态,直到条件满足才会继续执行。

非阻塞I/O则像是自助餐厅的就餐方式。你拿着餐盘去取食物,如果某个菜品暂时没有,你不会站在那里等待,而是先去拿其他食物,过一会儿再来查看。在编程中,这意味着当I/O操作无法立即完成时,操作会立即返回一个错误码(如EAGAIN),而不会阻塞进程。

代码示例对比:

// 阻塞方式读取串口
fd = open("/dev/ttyS1", O_RDWR);
read(fd, &buf, 1);  // 串口有输入才返回

// 非阻塞方式读取串口
fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
while(read(fd, &buf, 1) != 1) continue; // 循环尝试读取

1.2 两种模式的优缺点

阻塞I/O的优点

  • CPU利用率高,因为等待时不占用CPU资源
  • 编程模型简单直接
  • 适合顺序处理任务

阻塞I/O的缺点

  • 响应性差,无法同时处理多个I/O操作
  • 可能导致进程长时间挂起

非阻塞I/O的优点

  • 响应性好,可以同时监控多个I/O操作
  • 进程不会被长时间挂起
  • 适合高并发场景

非阻塞I/O的缺点

  • CPU利用率高,因为需要不断轮询
  • 编程复杂度较高
  • 可能导致忙等待(busy waiting)

2. 等待队列机制

2.1 等待队列的概念

等待队列是Linux内核中实现阻塞I/O的核心机制。它允许进程在条件不满足时进入睡眠状态,当条件满足时再被唤醒。这就像医院候诊室的叫号系统,病人(进程)可以坐着休息(睡眠),当轮到他们时会被叫醒(唤醒)。

2.2 等待队列的操作接口

  1. 定义和初始化等待队列头
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
// 或者使用宏一次性完成
DECLARE_WAIT_QUEUE_HEAD(my_queue);
  1. 定义等待队列项
DECLARE_WAITQUEUE(name, tsk); // tsk一般为current表示当前进程
  1. 添加/移除等待队列
add_wait_queue(&my_queue, &wait); // 添加
remove_wait_queue(&my_queue, &wait); // 移除
  1. 等待事件
wait_event(wq, condition); // 无条件等待
wait_event_timeout(wq, condition, timeout); // 带超时等待
wait_event_interruptible(wq, condition); // 可被信号中断的等待
  1. 唤醒队列
wake_up(&queue); // 唤醒所有等待的进程
wake_up_interruptible(&queue); // 只唤醒可中断的进程

2.3 等待队列的使用模板

一个典型的驱动中使用等待队列的模板如下:

static ssize_t device_read(struct file *file, char *buffer, size_t count, loff_t *ppos)
{
    DECLARE_WAITQUEUE(wait, current);
    add_wait_queue(&dev->read_queue, &wait);
    
    // 等待数据可用
    while (data_not_ready()) {
        if (file->f_flags & O_NONBLOCK) { // 非阻塞模式检查
            remove_wait_queue(&dev->read_queue, &wait);
            return -EAGAIN;
        }
        __set_current_state(TASK_INTERRUPTIBLE);
        schedule(); // 让出CPU
        if (signal_pending(current)) { // 检查是否有信号
            remove_wait_queue(&dev->read_queue, &wait);
            return -ERESTARTSYS;
        }
    }
    
    // 数据已准备好,进行读取操作
    copy_to_user(buffer, dev->data, count);
    
    remove_wait_queue(&dev->read_queue, &wait);
    set_current_state(TASK_RUNNING);
    return count;
}

3. 轮询机制

3.1 轮询的概念

对于非阻塞I/O,当操作不能立即完成时,应用程序需要通过轮询的方式不断检查设备是否就绪。这就像你等待快递时不断查看物流信息,而不是坐在门口一直等待。

3.2 常见的轮询机制

  1. select系统调用
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select使用位图(fd_set)来表示文件描述符集合,有以下操作宏:

FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符
FD_CLR(int fd, fd_set *set); // 移除描述符
FD_ISSET(int fd, fd_set *set); // 检查描述符
  1. poll系统调用
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll使用动态数组而非固定大小的位图,没有文件描述符数量限制。pollfd结构体定义如下:

struct pollfd {
    int fd; // 文件描述符
    short events; // 等待的事件
    short revents; // 实际发生的事件
};
  1. epoll机制
    epoll是为处理大并发而设计的更高效的机制,使用红黑树管理文件描述符,避免了select/poll的线性扫描问题。

3.3 轮询机制的选择

  • 少量文件描述符:select或poll都可以
  • 大量文件描述符:优先选择epoll
  • 跨平台需求:select兼容性最好
  • 精确事件通知:poll或epoll更合适

4. 驱动中的poll操作函数

4.1 poll操作函数的作用

在Linux驱动中,poll操作函数用于支持select/poll系统调用,它需要完成两个主要任务:

  1. 将当前文件描述符加入适当的等待队列
  2. 返回设备当前的状态掩码

4.2 poll函数的实现模板

static unsigned int device_poll(struct file *filp, poll_table *wait)
{
    struct device_data *dev = filp->private_data;
    unsigned int mask = 0;
    
    poll_wait(filp, &dev->read_queue, wait);
    poll_wait(filp, &dev->write_queue, wait);
    
    if (data_available(dev)) // 检查是否可读
        mask |= POLLIN | POLLRDNORM;
    
    if (space_available(dev)) // 检查是否可写
        mask |= POLLOUT | POLLWRNORM;
    
    return mask;
}

4.3 poll支持的事件标志

  • POLLIN:有普通或优先级带数据可读
  • POLLRDNORM:有普通数据可读
  • POLLRDBAND:有优先级带数据可读
  • POLLPRI:有高优先级数据可读
  • POLLOUT:写数据不会导致阻塞
  • POLLWRNORM:写普通数据不会导致阻塞
  • POLLWRBAND:写优先级带数据不会导致阻塞
  • POLLERR:发生错误
  • POLLHUP:设备已断开连接
  • POLLNVAL:文件描述符未打开

5. 阻塞I/O实验:实现一个FIFO设备驱动

5.1 实验目标

实现一个支持阻塞读写的全局FIFO设备驱动:

  • 当FIFO为空时,读进程阻塞
  • 当FIFO满时,写进程阻塞
  • 支持select/poll监控

5.2 关键数据结构

#define FIFO_SIZE 4096

struct globalfifo_dev {
    struct cdev cdev;
    unsigned int current_len;
    unsigned char mem[FIFO_SIZE];
    struct mutex mutex;
    wait_queue_head_t read_wait;
    wait_queue_head_t write_wait;
};

5.3 读函数实现

static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    struct globalfifo_dev *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);
    int ret = 0;
    
    mutex_lock(&dev->mutex);
    add_wait_queue(&dev->read_wait, &wait);
    
    while (dev->current_len == 0) {
        if (filp->f_flags & O_NONBLOCK) {
            ret = -EAGAIN;
            goto out;
        }
        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->mutex);
        schedule();
        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            goto out2;
        }
        mutex_lock(&dev->mutex);
    }
    
    if (count > dev->current_len)
        count = dev->current_len;
    
    if (copy_to_user(buf, dev->mem, count)) {
        ret = -EFAULT;
        goto out;
    }
    
    memmove(dev->mem, dev->mem + count, dev->current_len - count);
    dev->current_len -= count;
    wake_up_interruptible(&dev->write_wait);
    ret = count;
    
out:
    mutex_unlock(&dev->mutex);
out2:
    remove_wait_queue(&dev->read_wait, &wait);
    set_current_state(TASK_RUNNING);
    return ret;
}

5.4 写函数实现

static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    struct globalfifo_dev *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);
    int ret = 0;
    
    mutex_lock(&dev->mutex);
    add_wait_queue(&dev->write_wait, &wait);
    
    while (dev->current_len == FIFO_SIZE) {
        if (filp->f_flags & O_NONBLOCK) {
            ret = -EAGAIN;
            goto out;
        }
        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->mutex);
        schedule();
        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            goto out2;
        }
        mutex_lock(&dev->mutex);
    }
    
    if (count > FIFO_SIZE - dev->current_len)
        count = FIFO_SIZE - dev->current_len;
    
    if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
        ret = -EFAULT;
        goto out;
    }
    
    dev->current_len += count;
    wake_up_interruptible(&dev->read_wait);
    ret = count;
    
out:
    mutex_unlock(&dev->mutex);
out2:
    remove_wait_queue(&dev->write_wait, &wait);
    set_current_state(TASK_RUNNING);
    return ret;
}

5.5 poll函数实现

static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
    struct globalfifo_dev *dev = filp->private_data;
    unsigned int mask = 0;
    
    mutex_lock(&dev->mutex);
    poll_wait(filp, &dev->read_wait, wait);
    poll_wait(filp, &dev->write_wait, wait);
    
    if (dev->current_len != 0)
        mask |= POLLIN | POLLRDNORM;
    
    if (dev->current_len != FIFO_SIZE)
        mask |= POLLOUT | POLLWRNORM;
    
    mutex_unlock(&dev->mutex);
    return mask;
}

5.6 测试方法

  1. 加载驱动模块:
insmod globalfifo.ko
  1. 创建设备节点:
mknod /dev/globalfifo c 250 0
  1. 测试阻塞读:
# 终端1
cat /dev/globalfifo

# 终端2
echo "Hello World" > /dev/globalfifo
  1. 测试非阻塞读:
cat /dev/globalfifo &
# 应该立即返回,显示资源暂时不可用

6. 非阻塞I/O实验:按键驱动实现

6.1 实验目标

实现一个支持非阻塞读的按键驱动:

  • 当没有按键事件时,非阻塞读立即返回
  • 当有按键事件时,读取按键值
  • 支持中断处理

6.2 关键数据结构

struct key_dev {
    int gpio;
    int irq;
    char name[10];
    atomic_t key_value;
    atomic_t key_pressed;
    wait_queue_head_t waitq;
};

6.3 读函数实现

static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    struct key_dev *dev = filp->private_data;
    int ret;
    unsigned char value;
    
    if (filp->f_flags & O_NONBLOCK) {
        if (!atomic_read(&dev->key_pressed))
            return -EAGAIN;
    } else {
        wait_event_interruptible(dev->waitq, 
            atomic_read(&dev->key_pressed));
    }
    
    value = atomic_read(&dev->key_value);
    if (copy_to_user(buf, &value, 1))
        return -EFAULT;
    
    atomic_set(&dev->key_pressed, 0);
    return 1;
}

6.4 中断处理函数

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_dev *dev = dev_id;
    int gpio_value = gpio_get_value(dev->gpio);
    
    if (gpio_value == 0) { // 按键按下
        atomic_set(&dev->key_value, KEY_VALUE_PRESSED);
    } else { // 按键释放
        atomic_set(&dev->key_value, KEY_VALUE_RELEASED);
        atomic_set(&dev->key_pressed, 1);
        wake_up_interruptible(&dev->waitq);
    }
    
    return IRQ_HANDLED;
}

6.5 测试方法

  1. 加载驱动模块:
insmod key.ko
  1. 创建设备节点:
mknod /dev/key c 240 0
  1. 测试非阻塞读:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    int fd = open("/dev/key", O_RDONLY | O_NONBLOCK);
    char key_val;
    
    while (1) {
        if (read(fd, &key_val, 1) == 1) {
            printf("Key event: %d\n", key_val);
        } else {
            printf("No key event, doing other work...\n");
            sleep(1);
        }
    }
    
    close(fd);
    return 0;
}

7. 总结与选择建议

7.1 阻塞与非阻塞I/O对比

特性阻塞I/O非阻塞I/O
行为等待直到操作完成立即返回,成功或失败
CPU使用等待时不占用CPU需要主动轮询
响应性
编程复杂度简单较复杂
适用场景简单同步操作高并发或快速响应

7.2 选择建议

  1. 选择阻塞I/O当

    • 处理简单的顺序任务
    • 不需要同时处理多个I/O操作
    • 资源通常能快速就绪
  2. 选择非阻塞I/O当

    • 需要同时监控多个I/O操作
    • 要求快速响应
    • 处理高并发连接
  3. 对于驱动开发者

    • 通常需要同时支持阻塞和非阻塞模式
    • 正确实现等待队列和poll操作
    • 注意并发控制和竞态条件

通过理解阻塞和非阻塞I/O的原理和实现方式,开发者可以根据具体应用场景选择最合适的I/O模型,编写出高效可靠的Linux驱动程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2343266.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】MySQL索引与事务

目录 前言 1. 索引 &#xff08;index&#xff09; 1.1 概念 1.2 作用 1.3 使用场景 1.4 索引的相关操作 查看索引 创建索引 删除索引 2. 索引背后的数据结构 2.1 B树 2.2 B&#xff0b;树的特点 2.3 B&#xff0b;树的优势 3. 事务 3.1 为什么使用事务 3.2 事…

华为网路设备学习-19 IGP路由专题-路由策略

一、 二、 注意&#xff1a; 当该节点匹配模式为permit下时&#xff0c;参考if else 当该节点匹配模式为deny下时&#xff1a; 1、该节点中的apply子语句不会执行。 2、如果满足所有判断&#xff08;if-match&#xff09;条件时&#xff0c;拒绝该节点并跳出&#xff08;即不…

基于DrissionPage的表情包爬虫实现与解析(含源码)

目录 ​编辑 一、环境配置与技术选型 1.1 环境要求 1.2 DrissionPage优势 二、爬虫实现代码 三、代码解析 3.1 类结构设计 3.2 目录创建方法 3.3 图片链接获取 3.4 图片下载方法 四、技术升级对比 4.1 代码复杂度对比 4.2 性能测试数据 五、扩展优化建议 5.1 并…

区间和数量统计 之 前缀和+哈希表

文章目录 1512.好数对的数目2845.统计趣味子数组的数目1371.每个元音包含偶数次的最长子字符串 区间和的数量统计是一类十分典型的问题&#xff1a;记录左边&#xff0c;枚举右边策略前置题目&#xff1a;统计nums[j]nums[i]的对数进阶版本&#xff1a;统计子数组和%modulo k的…

全能 Sui 技术栈,构建 Web3 的未来

本文翻译自&#xff1a;FourPillarsFP&#xff0c;文章仅代表作者观点。 2025 年&#xff0c;SuiNetwork正在以一套全栈区块链策略强势出击&#xff0c;彻底打破加密行业的传统范式。正如 Mysten Labs 联合创始人 Adeniyi Abiodun 所说&#xff1a;“Sui 不只是一条区块链&…

linux安装单节点Elasticsearch(es),安装可视化工具kibana

真的&#xff0c;我安装个es和kibana&#xff0c;找了好多帖子&#xff0c;问了好几遍ai才安装成功&#xff0c;在这里记录一下&#xff0c;我相信&#xff0c;跟着我的步骤走&#xff0c;99%会成功&#xff1b; 为了让大家直观的看到安装过程&#xff0c;我把我服务器的es和ki…

RK3xxx 部分无法连接虚拟机 无法进行adb连接

我发现部分rk板子可以连接到虚拟机上&#xff0c;部分连接不上。其中尝试了一块是安卓系统的rk板子是可以连接虚拟机。但是用了linux系统的rk板子连接不上虚拟机。尝试了很多办法还是无法连接虚拟机。 然后也看到一些相关资料&#xff0c;但是太少了&#xff0c;只有这个链接提…

26考研——存储系统(3)

408答疑 文章目录 一、存储器概述二、主存储器三、主存储器与 CPU 的连接四、外部存储器五、高速缓冲存储器六、虚拟存储器七、参考资料鲍鱼科技课件26王道考研书 八、总结复习提示思考题常见问题和易混淆知识点 一、存储器概述 文章链接: 点击跳转 二、主存储器 文章链接: …

C# 实战_RichTextBox选中某一行条目高亮,离开恢复

C# 中控件richtextbox中某一行的条目内容高亮&#xff0c;未选中保持不变。当鼠标点击某一行的条目高亮&#xff0c;离开该条目就恢复默认颜色。 运行效果&#xff1a; 核心代码实现功能&#xff1a; //高亮指定行的方法private void HighlightLine(RichTextBox rtb,int lineI…

Servlet小结

视频链接&#xff1a;黑马servlet视频全套视频教程&#xff0c;快速入门servlet原理servlet实战 什么是Servlet&#xff1f; 菜鸟教程&#xff1a;Java Servlet servlet&#xff1a; server applet Servlet是一个运行在Web服务器&#xff08;如Tomcat、Jetty&#xff09;或应用…

2025上海车展:光峰科技全球首发“灵境”智能车载光学系统

当AI为光赋予思想&#xff0c;汽车将会变成什么样&#xff1f;深圳光峰科技为您揭晓答案。 2025年4月23日&#xff0c;在刚刚开幕的“2025上海车展”上&#xff0c;全球领先的激光核心器件公司光峰科技举办了主题为“AI光影盛宴&#xff0c;智享未来出行”的媒体发布会&#x…

BiliNote:开源的AI视频笔记生成工具,让知识提取与分享更高效——跨平台自动生成结构化笔记,实现从视频到Markdown的智能转化

引言:视频学习的痛点与BiliNote的解决方案 随着知识视频化趋势的加速,B站、YouTube等平台成为学习与信息获取的重要渠道,但手动记录笔记耗时低效、信息碎片化等问题依然突出。BiliNote的出现,通过AI驱动的自动化流程,将视频内容转化为结构清晰的Markdown笔记,支持截图插…

图纸安全防护管理:构建企业核心竞争力的关键屏障

在当今高度竞争的商业环境中&#xff0c;图纸作为企业核心技术的重要载体&#xff0c;其安全防护管理已成为企业知识产权保护体系中的关键环节。无论是建筑行业的施工蓝图、制造业的产品设计图&#xff0c;还是高科技企业的研发图纸&#xff0c;都承载着企业的核心竞争力和商业…

借助内核逻辑锁pagecache到内存

一、背景 内存管理是一个永恒的主题&#xff0c;尤其在内存紧张触发内存回收的时候。系统在通过磁盘获取磁盘上的文件的内容时&#xff0c;若不开启O_DIRECT方式进行读写&#xff0c;磁盘上的任何东西都会被缓存到系统里&#xff0c;我们称之为page cache。可以想象&#xff0…

Nacos简介—2.Nacos的原理简介

大纲 1.Nacos集群模式的数据写入存储与读取问题 2.基于Distro协议在启动后的运行规则 3.基于Distro协议在处理服务实例注册时的写路由 4.由于写路由造成的数据分片以及随机读问题 5.写路由 数据分区 读路由的CP方案分析 6.基于Distro协议的定时同步机制 7.基于Distro协…

TCP协议理解

文章目录 TCP协议理解理论基础TCP首部结构图示字段逐项解析 TCP是面向连接&#xff08;Connection-Oriented&#xff09;面向连接的核心表现TCP 面向连接的核心特性TCP 与UDP对比 TCP是一个可靠的(reliable)序号与确认机制&#xff08;Sequencing & Acknowledgment&#xf…

用 LangChain 手搓 RAG 系统:从原理到实战

一、RAG 系统简介 在当今信息爆炸的时代&#xff0c;如何高效地从海量数据中获取有价值的信息并生成准确、自然的回答&#xff0c;成为了人工智能领域的重要课题。检索增强生成&#xff08;Retrieval-Augmented Generation&#xff0c;RAG&#xff09;系统应运而生&#xff0c;…

联合体和枚举类型

1.联合体类型 1.1:联合体类型变量的创建 与结构体类型一样&#xff0c;联合体类型 (关键字:union) 也是由⼀个或者多个成员变量构成&#xff0c;这些成员变量既可以是不同的类型&#xff0c;也可以是相同的类型。但是编译器只为最⼤的成员变量分配⾜够的内存空间。联合体的特…

C语言指针5

1.void*概述 void称为无类型&#xff0c;void*称为无类型指针&#xff0c;void不可以单独定义变量&#xff0c;却可以定义无类型的指针&#xff0c;而且所定义的指针称为泛型指针&#xff0c;所谓泛型指针&#xff0c;其含义是void*类型的指针可以接收一切类型变量的地址 struc…

文档构建:Sphinx全面使用指南 — 强化篇

文档构建&#xff1a;Sphinx全面使用指南 — 强化篇 Sphinx 是一款强大的文档生成工具&#xff0c;使用 reStructuredText 作为标记语言&#xff0c;通过扩展兼容 Markdown&#xff0c;支持 HTML、PDF、EPUB 等多种输出格式。它具备自动索引、代码高亮、跨语言支持等功能&#…