Linux 阻塞和非阻塞 I/O 简明指南

news2025/5/12 19:04:52

目录

声明

1. 阻塞和非阻塞简介

2. 等待队列

2.1 等待队列头

2.2 等待队列项

2.3 将队列项添加/移除等待队列头

2.4 等待唤醒

2.5 等待事件

3. 轮询

3.1 select函数

3.2 poll函数

3.3 epoll函数

4. Linux 驱动下的 poll 操作函数

声明

本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 — 正点原子资料下载中心 1.0.0 文档,旨在如实记录我在学校学习该开发板过程中所遭遇的各类问题以及详细的解决办法。其初衷纯粹是为了个人知识梳理、学习总结以及日后回顾查阅方便,同时也期望能为同样在学习这款开发板的同学或爱好者提供一些解决问题的思路和参考。我尽力保证内容的准确性和可靠性,但由于个人知识水平和实践经验有限,若存在错误或不严谨之处,恳请各位读者批评指正。

责任声明:虽然我力求提供有效的问题解决办法,但由于开发板使用环境、硬件差异、软件版本等多种因素的影响,我的笔记内容不一定适用于所有情况。对于因参考本笔记而导致的任何直接或间接损失,我不承担任何法律责任。使用本笔记内容的读者应自行承担相关风险,并在必要时寻求专业技术支持。

1. 阻塞和非阻塞简介

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO (IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作)就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式 IO 如图所示:

应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。

非阻塞 IO 如图所示:

应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。

2. 等待队列

2.1 等待队列头

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:

// 定义一个等待队列头结构体
// 等待队列头用于管理等待队列,等待队列常用于实现进程的睡眠和唤醒操作
struct __wait_queue_head {
    // 自旋锁,用于保护等待队列相关的操作
    // 自旋锁是一种用于多处理器环境下的锁机制,当一个线程获取自旋锁时,如果锁已被其他线程持有,该线程会不断循环尝试获取锁,而不是进入睡眠状态
    spinlock_t lock; 
    // 任务列表,用于存储等待在该等待队列上的任务(进程)
    // 这里使用链表来管理等待的任务,每个等待的任务以节点的形式存储在该链表中
    struct list_head task_list; 
};

// 为结构体 __wait_queue_head 定义一个别名 wait_queue_head_t
// 这样在后续使用时可以更方便地声明该结构体类型的变量
typedef struct __wait_queue_head wait_queue_head_t; 

定义好等待队列头以后 使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:

void init_waitqueue_head(wait_queue_head_t *q)

参数 q 就是要初始化的等待队列头。

也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化

2.2 等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项,结构体内容如下:
 

// 等待队列结构体
struct __wait_queue {
    // 标志位,用于存储一些与等待队列相关的状态信息,例如等待的条件是否满足等
    unsigned int flags;
    // 私有数据指针,通常用于指向与等待队列相关的特定数据,例如等待的进程相关的额外信息等
    void *private;
    // 等待队列函数指针,指向一个函数,该函数通常用于处理等待队列上的事件,比如唤醒等待的任务等
    wait_queue_func_t func;
    // 任务列表,用于将等待队列上的任务以链表的形式组织起来,方便管理和操作等待的任务
    struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:

DECLARE_WAITQUEUE(name, tsk)

name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

2.3 将队列项添加/移除等待队列头

当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,

等待队列项添加 API 函数如下:

void add_wait_queue(wait_queue_head_t *q,
                    wait_queue_t *wait)

函数参数和返回值含义如下:
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除 API 函数如下:

void remove_wait_queue(wait_queue_head_t *q,
                        wait_queue_t *wait)

函数参数和返回值含义如下:
q: 要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。

2.4 等待唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用两个函数:

void wake_up(wait_queue_head_t *q)

void wake_up_interruptible(wait_queue_head_t *q)

参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

2.5 等待事件

当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的 API 函数如表 52.1.2.1 所示:

3. 轮询

如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。 poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行。

3.1 select函数

select 函数原型:

// nfds 表示被检查的描述符集合中最大描述符加1
// 即它用于指定需要检查的文件描述符的范围,范围是从0到nfds - 1的文件描述符都会被检查
int select(int nfds, 
           // readfds 是一个指向fd_set类型的指针,fd_set 是一个用于存储文件描述符集合的类型
           // readfds 用于指定需要检查可读性的文件描述符集合,集合中的文件描述符会被检查是否有数据可读
           fd_set *readfds, 
           // writefds 是一个指向fd_set类型的指针
           // writefds 用于指定需要检查可写性的文件描述符集合,集合中的文件描述符会被检查是否可写
           fd_set *writefds, 
           // exceptfds 是一个指向fd_set类型的指针
           // exceptfds 用于指定需要检查异常情况的文件描述符集合,集合中的文件描述符会被检查是否有异常发生
           fd_set *exceptfds, 
           // timeout 是一个指向struct timeval类型的指针
           // struct timeval 是一个结构体,包含秒和微秒两个成员,用于表示时间
           // timeout 用于设置select函数的超时时间,如果设置为NULL,则select函数会一直阻塞直到有文件描述符满足条件或发生错误;如果设置了具体的时间值,select函数会在超时时间到达时返回
           struct timeval *timeout) 
{
    // 函数体部分省略,实际的select函数实现会根据传入的参数检查文件描述符集合
    // 并返回满足条件的文件描述符数量,或者在超时或发生错误时返回相应的值
}

select 函数的返回值有以下几种情况:

大于 0:表示有文件描述符满足了可读、可写或异常的条件,返回值是满足条件的文件描述符的数量。
等于 0:表示在指定的超时时间内没有文件描述符满足条件(当 timeout 不为 NULL 时)。
小于 0:表示发生了错误,具体的错误类型可以通过 errno 变量获取

nfds: 所要监视的这三类文件描述集合中, 最大文件描述符加 1。

readfds、 writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的 fd_set 类型变量的每一个位都代表了一个文件描述符。 readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。 writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。 exceptfds 用于监视这些文件的异常。

当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

FD_ZERO 用于将 fd_set 变量的所有位都清零。

FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。

FD_CLR 用于将 fd_se变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。

FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:

// 定义一个表示时间值的结构体 timeval
struct timeval {
    // tv_sec 成员表示秒数,是一个长整型数据
    // 用于存储时间值中的秒部分
    long tv_sec; /* 秒 */
    // tv_usec 成员表示微秒数,也是一个长整型数据
    // 用于存储时间值中的微秒部分,微秒是秒的百万分之一
    long tv_usec; /* 微秒 */
};

当 timeout 为 NULL 的时候就表示无限期的等待。

返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数。

使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
 

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>

// 主函数
void main(void)
{
    int ret, fd; // 定义变量 ret 用于存储 select 函数的返回值,fd 用于存储文件描述符
    fd_set readfds; // 定义文件描述符集 readfds,用于存储需要监视的读操作文件描述符
    struct timeval timeout; // 定义超时结构体 timeout,用于设置 select 函数的超时时间

    // 以读写且非阻塞的方式打开文件 "dev_xxx",将返回的文件描述符存储在 fd 中
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); 
    
    // 清空文件描述符集 readfds,将所有位都设置为 0
    FD_ZERO(&readfds); 
    
    // 将文件描述符 fd 添加到文件描述符集 readfds 中,表示要监视 fd 的读操作
    FD_SET(fd, &readfds); 
    
    // 设置超时结构体的秒数为 0
    timeout.tv_sec = 0; 
    
    // 设置超时结构体的微秒数为 500000,即 500 毫秒
    timeout.tv_usec = 500000; 
    
    // 调用 select 函数,监视 readfds 集合中的文件描述符的读操作,
    // 第二个参数为 NULL 表示不监视写操作,第三个参数为 NULL 表示不监视异常情况,
    // 最后一个参数为 timeout 表示设置的超时时间,将返回满足条件的文件描述符数量或错误值
    ret = select(fd + 1, &readfds, NULL, NULL, &timeout); 

    // 根据 select 函数的返回值进行处理
    switch (ret) {
        case 0: // 如果返回值为 0,表示超时
            printf("timeout!\r\n");
            break;
        case -1: // 如果返回值为 -1,表示发生错误
            printf("error!\r\n");
            break;
        default: // 其他情况表示有文件描述符满足可读条件
            if(FD_ISSET(fd, &readfds)) { // 检查文件描述符 fd 是否在 readfds 集合中且满足可读条件
                // 这里可以添加使用 read 函数读取数据的代码,目前仅作为示例,实际使用中需要根据需求编写读取数据的逻辑
                // read(fd, buffer, sizeof(buffer)); // 假设 buffer 是用于存储读取数据的缓冲区
            }
            break;
    }
}

注:在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数。

3.2 poll函数

poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制, Linux 应用程序中 poll 函数原型如下所示:
 

int poll(struct pollfd *fds,
                nfds_t nfds,
                int timeout)

fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的, pollfd 结构体如下所示:

// pollfd 结构体用于描述一个被监视的文件描述符及其相关事件
struct pollfd {
    // 文件描述符:需要监视的目标文件描述符
    // 设置为 -1 时表示忽略该条目(events 字段会被忽略,revents 会被置零)
    int fd;         
    
    // 请求的事件:bitmask,指定要监视的事件类型
    // 常用事件标志包括:
    //   POLLIN   - 有数据可读(包括普通数据和优先带外数据)
    //   POLLOUT  - 可以写入数据(不会阻塞)
    //   POLLPRI  - 有紧急数据可读(如 TCP 带外数据)
    //   POLLERR  - 发生错误(仅在 revents 中返回,不可设置)
    //   POLLHUP  - 发生挂起(如管道关闭、连接断开)
    //   POLLNVAL - 文件描述符无效(如未打开)
    short events;   
    
    // 返回的事件:bitmask,由内核填充,指示实际发生的事件
    // 包含与 events 相同的标志位,另外还可能包含:
    //   POLLERR  - 文件描述符发生错误
    //   POLLHUP  - 文件描述符被挂起
    //   POLLNVAL - 文件描述符无效
    short revents;  
};

fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents
返回 0。 events 是要监视的事件。返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并且设置 errno 为错误类型。

使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>

int main(void)
{
    int ret;
    int fd;                     /* 要监视的文件描述符 */
    struct pollfd fds;          /* 定义pollfd结构体,用于poll系统调用 */

    // 以读写和非阻塞模式打开指定文件
    // 非阻塞模式意味着read/write操作不会阻塞进程
    fd = open("filename", O_RDWR | O_NONBLOCK); 
    if (fd < 0) {
        perror("open failed");
        return -1;
    }

    /* 配置pollfd结构体 */
    fds.fd = fd;                /* 设置要监视的文件描述符 */
    fds.events = POLLIN;        /* 监视POLLIN事件:表示有数据可读 */
                                /* 可选事件还包括:POLLOUT(可写)、POLLERR(错误)等 */

    /* 调用poll函数监视文件描述符 */
    // 参数1:指向pollfd数组的指针(这里只有一个元素)
    // 参数2:数组中元素的数量
    // 参数3:超时时间(毫秒),-1表示无限等待,0表示立即返回
    ret = poll(&fds, 1, 500);   /* 轮询文件是否有可读数据,超时时间500ms */

    /* 处理poll返回值 */
    if (ret > 0) {              /* 有事件发生 */
        if (fds.revents & POLLIN) {  /* 检查是否是可读事件 */
            /* 读取数据的代码 */
            char buffer[1024];
            int bytes = read(fd, buffer, sizeof(buffer));
            if (bytes > 0) {
                printf("Read %d bytes\n", bytes);
                /* 处理读取到的数据 */
            } else if (bytes == 0) {
                printf("EOF detected\n");
            } else {
                perror("read error");
            }
        } else if (fds.revents & POLLERR) {  /* 检查是否有错误发生 */
            printf("Poll error on fd %d\n", fd);
        }
    } else if (ret == 0) {      /* 超时,没有事件发生 */
        printf("Poll timeout after 500ms\n");
    } else {                    /* 错误处理 */
        perror("poll failed");
        close(fd);
        return -1;
    }

    close(fd);                  /* 关闭文件描述符 */
    return 0;
}

3.3 epoll函数

selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。

epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄, epoll_create 函数原型如下:

int epoll_create(int size)

size: 从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。

返回值: epoll 句柄,如果为-1 的话表示创建失败。

epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件, epoll_ctl 函数原型如下所示:

/**
 * epoll_ctl - 控制epoll实例,添加、修改或删除监视的文件描述符
 * @epfd:   epoll实例的文件描述符(由epoll_create创建)
 * @op:     操作类型,可选值:
 *          - EPOLL_CTL_ADD: 向epoll实例中添加新的监视文件描述符
 *          - EPOLL_CTL_MOD: 修改已存在的监视文件描述符的事件掩码
 *          - EPOLL_CTL_DEL: 从epoll实例中删除监视的文件描述符
 * @fd:     要操作的目标文件描述符(如socket、管道等)
 * @event:  指向epoll_event结构体的指针,指定要监视的事件类型和关联数据
 *          当op为EPOLL_CTL_DEL时,event参数可以为NULL
 * 
 * 返回值:
 * 成功时返回0,失败时返回-1并设置errno
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数 struct epoll_event *event 

struct epoll_event {
    uint32_t     events;      /* 要监视的事件类型(位掩码) */
    epoll_data_t data;        /* 与文件描述符关联的用户数据 */
};

typedef union epoll_data {
    void        *ptr;         /* 指向用户自定义数据的指针 */
    int          fd;          /* 文件描述符 */
    uint32_t     u32;         /* 32位整数 */
    uint64_t     u64;         /* 64位整数 */
} epoll_data_t;

 常用事件类型:

常用事件类型(events 字段):
EPOLLIN - 有数据可读(包括普通数据和优先带外数据)
EPOLLOUT - 可以写入数据(不会阻塞)
EPOLLPRI - 有紧急数据可读(如 TCP 带外数据)
EPOLLERR - 文件描述符发生错误(自动触发,无需设置)
EPOLLHUP - 文件描述符被挂起(自动触发,无需设置)
EPOLLET - 设置为边沿触发模式(Edge Triggered,默认是水平触发)
EPOLLONESHOT - 一次性触发,事件触发后自动从监视列表移除,当监视完成以后还需要再次监视某个 fd,那么就需要将fd 重新添加到 epoll 里面。

返回值: 0,成功; -1,失败,并且设置 errno 的值为相应的错误码。

一切都设置好以后应用程序就可以通过 epoll_wait 函数来等待事件的发生,类似 select 函数。 epoll_wait 函数原型如下所示:

int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout)

函数参数和返回值含义如下:
epfd: 要等待的 epoll。
events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调
用者可以根据 events 判断发生了哪些事件。
maxevents: events 数组大小,必须大于 0。
timeout: 超时时间,单位为 ms。
返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量。
epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当
设计到的文件描述符(fd)比较少的时候就适合用 selcet 和 poll。

使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main() {
    // 创建epoll实例,参数size已被弃用但必须为正数(内核2.6.8后被忽略)
    // 返回值是一个文件描述符,用于后续的epoll操作
    int epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        close(epfd);  // 清理已创建的资源
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址(实际应用中需要bind、listen等操作)
    struct sockaddr_in server_addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8080),
        .sin_addr.s_addr = INADDR_ANY
    };

    // 绑定套接字(示例中省略错误处理)
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }

    // 监听连接(示例中省略错误处理)
    if (listen(sockfd, 5) == -1) {
        perror("listen");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }

    // 设置epoll_event结构体,配置监听事件
    struct epoll_event ev;
    // 监视可读事件并使用边沿触发模式
    // 边沿触发模式要求:
    // 1. 必须使用非阻塞I/O
    // 2. 必须处理完所有数据(否则不会再次触发)
    ev.events = EPOLLIN | EPOLLET;
    // 存储与事件关联的数据(可在epoll_wait返回时获取)
    ev.data.fd = sockfd;

    // 将监听套接字添加到epoll实例中
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("epoll_ctl: add");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }

    // 后续可能需要修改监听事件(例如添加可写事件)
    // 注意:修改事件时需重新设置整个ev.events字段
    ev.events = EPOLLIN | EPOLLOUT | EPOLLET;  // 同时监听可读和可写事件
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1) {
        perror("epoll_ctl: mod");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }

    // 当不再需要监听某个文件描述符时,从epoll实例中删除
    // 第三个参数为需要删除的文件描述符
    // 第四个参数在删除时可以为NULL(内核不使用该参数)
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL) == -1) {
        perror("epoll_ctl: del");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }

    // 关闭资源
    close(sockfd);
    close(epfd);
    return 0;
}

4. Linux 驱动下的 poll 操作函数

当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数, poll 函数原型如下所示:

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

函数参数和返回值含义如下: filp: 要打开的设备文件(文件描述符)。

wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。

返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:

POLLIN 有数据可以读取。

POLLPRI有紧急的数据需要读取。
POLLOUT可以写数据。
POLLERR指定的文件描述符发生错误。
POLLHUP指定的文件描述符挂起。
POLLNVAL无效的请求。
POLLRDNORM等同于 POLLIN,普通数据可读

在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中, poll_wait 函数原型如下:

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

参数 wait_address 是要添加到 poll_table 中的等待队列头,参数 p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数。

使用示例:

/**
 * my_driver_poll - 实现设备驱动的轮询方法,用于I/O多路复用
 * @filp: 指向被操作文件的结构体指针,包含文件状态和私有数据
 * @wait: 用于注册等待队列的poll_table结构体指针,由内核提供
 * 
 * 返回值:
 * 位掩码,表示当前设备的就绪状态(POLLIN/POLLOUT等)
 * 
 * 功能说明:
 * 1. 注册等待队列,使当前进程可以在设备状态变化时被唤醒
 * 2. 检查设备的读写缓冲区状态,设置相应的就绪标志
 * 3. 立即返回当前状态,不阻塞进程
 */
static unsigned int my_driver_poll(struct file *filp, struct poll_table_struct *wait)
{
    // 从文件的私有数据中获取设备结构体实例
    struct my_device *dev = filp->private_data;
    unsigned int mask = 0;  // 初始化就绪状态掩码

    /* 注册等待队列 - 这是实现非阻塞轮询的关键 */
    // 将当前进程添加到读操作的等待队列中
    // 当设备有新数据可读时,会唤醒该队列中的进程
    poll_wait(filp, &dev->read_wait_queue, wait);
    
    // 将当前进程添加到写操作的等待队列中
    // 当设备有足够空间可写入时,会唤醒该队列中的进程
    poll_wait(filp, &dev->write_wait_queue, wait);

    /* 检查读缓冲区状态 - 决定是否返回可读标志 */
    // 如果接收缓冲区不为空(有数据可读)
    if (!list_empty(&dev->rx_buffer)) {
        // 设置可读标志:
        // POLLIN      - 有数据可读(包括普通数据和优先数据)
        // POLLRDNORM  - 有普通数据可读(与POLLIN类似,具体取决于设备类型)
        mask |= POLLIN | POLLRDNORM;
    }

    /* 检查写缓冲区状态 - 决定是否返回可写标志 */
    // 如果发送缓冲区有足够空间(大于最小写入空间阈值)
    if (dev->tx_buffer_space > MIN_WRITE_SPACE) {
        // 设置可写标志:
        // POLLOUT     - 可以写入数据(不会阻塞)
        // POLLWRNORM  - 可以写入普通数据(与POLLOUT类似,具体取决于设备类型)
        mask |= POLLOUT | POLLWRNORM;
    }

    // 返回当前设备的就绪状态掩码
    // 内核会根据这个掩码通知用户空间程序哪些操作可以立即执行
    return mask;
}


 

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

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

相关文章

Java开发经验——阿里巴巴编码规范经验总结2

摘要 这篇文章是关于Java开发中阿里巴巴编码规范的经验总结。它强调了避免使用Apache BeanUtils进行属性复制&#xff0c;因为它效率低下且类型转换不安全。推荐使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手动赋值等替代方案。文章还指出不应在视图模板中加入复杂逻…

机器人手臂“听不懂“指令?Ethercat转PROFINET网关妙解通信僵局

机器人手臂"听不懂"指令&#xff1f;Ethercat转PROFINET网关妙解产线通信僵局 协作机器人&#xff08;如KUKA iiWA&#xff09;使用EtherCAT控制&#xff0c;与Profinet主站&#xff08;如西门子840D CNC&#xff09;同步动作。 客户反馈&#xff1a;基于Profinet…

深度学习 CNN

CNN 简介 什么是 CNN&#xff1f; 卷积神经网络&#xff08;Convolutional Neural Network&#xff09;是专为处理网格数据&#xff08;如图像&#xff09;设计的神经网络。核心组件&#xff1a; 卷积层 &#xff1a;提取局部特征&#xff08;如边缘、纹理&#xff09;通过卷…

MySQL索引原理以及SQL优化(二)

目录 1. 索引与约束 1.1 索引是什么 1.2 索引的目的 1.3 索引分类 1.3.1 数据结构 1.3.2 物理存储 1.3.3 列属性 1.3.4 列的个数 1.4 主键的选择 1.5 索引使用场景 1.6 索引的底层实现 1.6.1 索引存储 1.6.2 页 1.6.3 B 树 1.6.4 B 树层高问题 1.6.5 自增 id 1.7 innod…

MATLAB中矩阵和数组的区别

文章目录 前言环境配置1. 数据结构本质2. 运算规则&#xff08;1&#xff09;基本运算&#xff08;2&#xff09;特殊运算 3. 函数与操作4. 高维支持5. 创建方式 前言 在 MATLAB 中&#xff0c;矩阵&#xff08;Matrix&#xff09; 和 数组&#xff08;Array&#xff09; 的概…

Desfire Ev1\Ev2\Ev3卡DES\3K3DES\AES加解密读写C#示例源码

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.1d292c1bYhsS9c&ftt&id917152255720 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using S…

MySQL核心内容【完结】

MySQL核心内容 文章目录 MySQL核心内容1.MySQL核心内容目录2.MySQL知识面扩展3.MySQL安装4.MySQL配置目录介绍Mysql配置远程ip连接 5.MySQL基础1.MySQL数据类型1.数值类型2.字符串类型3.日期和时间类型4.enum和set 2.MySQL运算符1.算数运算符2.逻辑运算符3.比较运算符 3.MySQL完…

C++类和对象进阶 —— 与数据结构的结合

&#x1f381;个人主页&#xff1a;工藤新一 &#x1f50d;系列专栏&#xff1a;C面向对象&#xff08;类和对象篇&#xff09; &#x1f31f;心中的天空之城&#xff0c;终会照亮我前方的路 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 文章目录 […

Django之账号登录及权限管理

账号登录及权限管理 目录 1.登录功能 2.退出登录 3.权限管理 4.代码展示合集 这篇文章, 会讲到如何实现账号登录。账号就是我们上一篇文章写的账号管理功能, 就使用那里面已经创建好的账号。这一次登录, 我们分为三种角色, 分别是员工, 领导, 管理员。不同的角色, 登录进去…

EXCEL中嵌入其他表格等文件

在EXCEL中嵌入其他表格 先放链接&#xff1a;https://jingyan.baidu.com/article/295430f11708c34d7e00509a.html 步骤如下&#xff1a; 1、打开一个需要嵌入新表格的excel表。 2、切换至“插入”菜单中&#xff0c;单击选择“对象”。 3、如下图所示&#xff0c;会弹出“对象…

21. LangChain金融领域:合同审查与风险预警自动化

引言&#xff1a;当AI成为24小时不眠的法律顾问 2025年某商业银行的智能合同系统&#xff0c;将百万级合同审查时间从平均3周缩短至9分钟&#xff0c;风险条款识别准确率达98.7%。本文将基于LangChain的金融法律框架&#xff0c;详解如何构建合规、精准、可追溯的智能风控体系…

Springboot使用事件流调用大模型接口

什么是事件流 事件流&#xff08;Event Stream&#xff09; 是一种处理和传递事件的方式&#xff0c;通常用于系统中的异步消息传递或实时数据流。在事件驱动架构&#xff08;Event-Driven Architecture&#xff09;中&#xff0c;事件流扮演着至关重要的角色。 事件流的概念…

计算机网络--2

TCP三次握手 TCP连接为什么需要三次握手 1. 由于网络情况复杂,可能会出现丢包现象,如果第二次握手的时候服务器就认为这个端口可用,然后一直开启,但是如果客户端未收到服务器发送的回复,那么就会重新发送请求,服务器就会重新开启一个端口连接,这样就会浪费一个端口。 三…

尤雨溪宣布:Vue 生态正式引入 AI

在前端开发领域,Vue 框架一直以其易用性和灵活性受到广大开发者的喜爱。 而如今,Vue 生态在人工智能(AI)领域的应用上又迈出了重要的一步。 尤雨溪近日宣布,Vue、Vite 和 Rolldown 的文档网站均已添加了llms.txt文件,这一举措旨在让大型语言模型(LLM)更方便地理解这些…

蓝桥杯第十六届c组c++题目及个人理解

本篇文章只是部分题目的理解&#xff0c;代码和思路仅供参考&#xff0c;切勿当成正确答案&#xff0c;欢迎各位小伙伴在评论区与博主交流&#xff01; 题目&#xff1a;2025 题目解析 核心提取 要求的数中至少有1个0、2个2、1个5 代码展示 #include<iostream> #incl…

硬件工程师笔记——电子器件汇总大全

目录 1、电阻 工作原理 欧姆定律 电阻的物理本质 一、限制电流 二、分压作用 三、消耗电能&#xff08;将电能转化为热能&#xff09; 2、压敏电阻 伏安特性 1. 过压保护 2. 电压调节 3. 浪涌吸收 4. 消噪与消火花 5. 高频应用 3、电容 工作原理 &#xff08;…

微软推动智能体协同运作:支持 A2A、MCP 协议

今日凌晨&#xff0c;微软宣布 Azure AI Foundry 和 Microsoft Copilot Studio 两大开发平台支持最新 Agent 开发协议 A2A&#xff0c;并与谷歌合作开发扩大该协议&#xff0c;这一举措对智能体赛道意义重大。 现状与变革意义 当前智能体领域类似战国时代&#xff0c;各家技术…

Linxu实验五——NFS服务器

一.NFS服务器介绍 NFS服务器&#xff08;Network File System&#xff09;是一种基于网络的分布式文件系统协议&#xff0c;允许不同操作系统的主机通过网络共享文件和目录3。其核心作用在于实现跨平台的资源透明访问&#xff0c;例如在Linux和Unix系统之间共享静态数据&#…

20242817李臻《Linux⾼级编程实践》第9周

20242817李臻《Linux⾼级编程实践》第9周 一、AI对学习内容的总结 第十章 Linux下的数据库编程 10.1 MySQL数据库简介 MySQL概述&#xff1a;MySQL是一个开源的关系型数据库管理系统&#xff0c;最初由瑞典MySQL AB公司开发&#xff0c;后经SUN公司收购&#xff0c;现属于O…

开源分享:TTS-Web-Vue系列:SSML格式化功能与高级语音合成

&#x1f3af; 本文是TTS-Web-Vue系列的第十二篇文章&#xff0c;重点介绍项目新增的SSML格式化功能以及SSML在语音合成中的应用。通过自动格式化和实时预览&#xff0c;我们显著提升了SSML编辑体验&#xff0c;让用户能够更精确地控制语音合成的细节&#xff0c;实现更自然、更…