Linux驱动阻塞与非阻塞访问的理解

一、阻塞I/O

应用程序的阻塞访问方式

复制代码
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR);
ret = read(fd, &data, sizeof(data));

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,这样可以将CPU资源让出来,直到等待的条件被满足。而设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成。

示例:阻塞方式读取鼠标

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd, ret;

    fd = open("/dev/input/event3", O_RDONLY)
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    memset(buf, 0, sizeof(buf));
    ret = read(fd, buf, sizeof(buf));
    if (0 > ret) {
        perror("read error");
        close(fd);
        exit(-1);
    }

    printf("suceess to read<%d> bytes data\n", ret);
    
    close(fd);
    exit(0);
}

二、非阻塞I/O

应用程序的非阻塞访问方式

复制代码
int fd;
int data - 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
ret = read(fd, &data, sizeof(data));

示例:非阻塞方式读取鼠标

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd, ret;

    fd = open("/dev/input/event3", O_RDONLY|O_NONBLOCK)
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    memset(buf, 0, sizeof(buf));
    for(;;) {
        ret = read(fd, buf, sizeof(buf));
        if (0 < ret) {
            printf("suceess to read<%d> bytes data\n", ret);
            close(fd);
            exit(0);
        }
    }
}

三、驱动程序对阻塞进程的唤醒

等待队列wait queue实现了阻塞进程的唤醒工作。

等待队列要用到的数据结构:

1、使用等待队列,首先创建和初始化一个等待队列头,队列头结构体定义如下:

复制代码
struct __wait_queue_head {
    spinlock_t    lock;
    struct list_head    task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

2、每一个要访问设备的进程是一个队列项,队列项结构体:

复制代码
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;

等待队列API函数:

1、初始化等待队列头:

void init_waitqueue_head(wait_queue_head_t *q);

使用宏 DECLARE_WAIT_QUEUE_HEAD直接一次性完成定义和初始化,如下:

DECLARE_WAIT_QUEUE_HEAD(q);

2、初始化等待队列项:

使用宏DECLARE_WAITQUEUE可定义和初始化一个等待队列项:

DECLARE_WAITQUEUE(wait, current);

current是当前进程。

3、将队列项添加到等待队列头:

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

4、将队列项移除等待队列头:

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

5、唤醒等待队列头

唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程

void wake_up(wait_queue_head_t *q);

唤醒处于TASK_INTERRUPTIBLE状态的进程:

void wake_up_interruptible(wait_queue_head_t *q);

6、等待事件

等待以wq为等待队列头的等待队列被唤醒,将进程设置为TASK_INTERRUPTIBLE可被信号打断:

wait_event_interruptible(wq,condition)

增加了超时时间,以jiffers为单位,如果返回0表示超时时间到且conditon为假 :

wait_event_interruptible(wq,condition,timeout)

这些宏在内核头文件include/linux/wait.h中。

示例:设备驱动中使用等待队列

复制代码
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
    
    ret = wait_event_interruptible(dev->r_wait, condition);
    
    DECLARE_WAITQUEUE(wait, current);    
    add_wait_queue(&dev->r_wait, &wait);

    avail = device_readable(...);
    if (avail < 0) {
        _set_current_state(TASK_INTERRUPTIBLE);
        schedule();
        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            goto out;
        }
        __set_current_state(TASK_RUNNING);
        remove_wait_queue(&dev->r_wait, &wait);
    }

    device_read(...);

out:
    remove_wait_queue(&dev->r_wait, &wait);
    __set_current_state(TASK_RUNNING);
}

四、非阻塞I/O应用程序轮询

应用程序通过select、epoll、poll函数来查询设备是否可以操作。

1、select

复制代码
int select(int             nfds,
           fd_set          *readfds,
           fd_set          *writedfds,
           fd_set          *exceptfds,
           struct timeval  *timeout);

nfds:三类文件描述符集合里最大值加一,文件描述符从 0 开始编号,select 需要检查的范围是 [0, nfds),即 0 到 nfds - 1。

fd_set类型变量的每一位对应一个fd:

readfds时监视描述符集的读变化;

writefds监视文件是否可写;

exceptfds监视文件的异常。

上面的三个参数如果设置为NULL就表示不关心这些文件的变化。

fd_set变量的操作宏,定义在 sys/select.h:

FD_ZERO(fdsetp)

FD_SET(fd, fdsetp)

FD_CLR(fd, fdsetp)

FD_ISSET(fd, fdsetp)

timeout设置超时时间,如果时NULL表示无期限等待。

复制代码
struct timeval {
    long     tv_sec;
    long     tv_usce;
};

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

示例:应用程序使用select函数非阻塞访问读

复制代码
void main(void)
{
    int ret, fd;
    fd_set readfds;
    struct timeval timeout;

    fd = open("dev_xxx", O_RDWR | O_NONBLOCK);

    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */

    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch(ret) {
        case 0:
            printf("timeout!\r\n");
            break;
        case -1: 
            printf("error!\r\n");
            break;
        default:
            if (FD_ISSET(fd, &readfds)) {
                /* read data */
            }
            break;
     }
}

2、poll

单个线程中有,select函数能够监视的文件描述符数最大限制为1024,这时就可以使用poll函数,poll函数没有最大文件描述符限制。

复制代码
int poll(struct pollfd  *fds,
          nfds_t         nfds,
          int            timeout)

nfds是要监视的文件描述符数量;

timeout单位是ms;

fds是要监视的文件描述符集合以及要监视的事件。

复制代码
struct pollfd {
    int   fd;
    short events;
    short revents;
};

fd是要监视的文件描述符;

events是要监视的事件:

revents是返回参数,即返回的事件。

poll函数的返回值

返回-1表示有错误发生,并且会设置errno;返回0表示该调用在任意一个文件描述符成为就绪态之前就超时了;返回一个正整数表示有一个或多个文件描述符处于就绪态了,返回值表示fds数组中返回的revents变量不为0的struct pollfd对象的数量。

示例:应用程序使用poll函数非阻塞访问读

复制代码
void main(void)
{
    int ret;
    int fd;
    struct pollfd fds;

    fd = open(filename, O_RDWR | O_NONBLOCK);

    fds.fd = fd;
    fds.events = POLLIN;

    ret = poll(&fds, 1, 500);
    if (ret) {
        [...]
        /* read data */
        [...]
    } else if (ret == 0) {
        /* timer out */
    } else if (ret < 0) {
        /* error */
    }
}

五、驱动程序对select和poll非阻塞访问的行为

设备驱动程序提供非阻塞处理方式也就是轮询。当应用程序调用select或poll函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行。

复制代码
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait)

filp是要打开的设备文件(文件描述符);

wait是由应用程序传递进来的,一般将此函数传递给poll_wait函数。

复制代码
void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)

参数wait_address是要添加到poll_table中的等待队列头;

参数p是file_operations中poll函数的wait参数。

示例:设备驱动poll()函数

复制代码
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
    
    if (filp->f_flags & O_NONBLOCK) {
        avail = device_readable(...);
        if (avail < 0) {
            ret = -EAGAIN;
        } else {
            ret = wait_event_interruptible(dev->r_wait, condition);
            if (ret) {
                retrun ret;
            }
        }
    }

    device_read(...);
    ret 0;
}

unsigned int xxx_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
    
    poll_wait(filp, &dev->r_wait, wait);
    avail = device_readable(...);
    if (avail) 
        mask = POLLIN | POLLRDNORM;

    return mask;
}
相关推荐
..Dauntless..1 小时前
【Linux】进程地址空间初步理解
linux·运维·服务器
Julien20041 小时前
Docker 网络(一)
linux·运维·服务器·ssh·学习方法
杨云龙UP2 小时前
一次数据库查询缓慢故障复盘:大表数据增长、SQL全表扫描导致系统响应异常
linux·运维·服务器·数据库·sql·mysql
PellyKoo2 小时前
【linux运维】ubuntu+samba 用户组独占目录权限配置踩坑记
linux·运维·ubuntu
AR-26710-3 小时前
Linux Day15——系统管理复习
linux·运维
高山有多高3 小时前
【Linux笔记】Linux自定义Shell
linux·c++
姜鱼问生3 小时前
HTTPS 部署实战:未备案域名 + 非标端口的完整方案
linux·https
jackletter3 小时前
linux:安装 ubuntu 26.04.1
linux·ubuntu·ubuntu26.04.1
2401_869769594 小时前
make 进度条
linux