HaveFunWithEmbeddedSystem/Chapter8_SOC_与_Linux/8.8_设备驱动中的阻塞与非阻塞_IO.md

15 KiB
Raw Permalink Blame History

8.8 设备驱动中的阻塞与非阻塞 IO

阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。

阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费 CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源让给其他进程。

因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。

等待队列

可以使用等待队列wait queue来实现阻塞进程的唤醒它以队列为基础数据结构与进程调度机制紧密结合能够用于实现内核中的异步事件通知机制。

等待队列头

// 定义等待队列头
wait_queue_head_t my_queue;
// 初始化等待队列头
init_waitqueue_head(&my_queue);
// 初始化并声明等待队列头
DECLARE_WAIT_QUEUE_HEAD(name)

定义和添加/移除等待队列

// 定义等待队列
wait_queue_t wait;
// 初始化等待队列,一般将 current 作为第二个参数
void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p);
// 初始化并声明等待队列
DECLARE_WAITQUEUE(name, tsk)

// 添加/移除等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

add_wait_queue() 用于将等待队列 wait 添加到等待队列头 q 指向的等待队列链表中,而 remove_wait_queue() 用于将等待队列 wait 从附属的等待队列头 q 指向的等待队列链表中移除。

add_wait_queue_exclusive() 会将等待队列元素加入到等待队列的尾部,并设置等待队列元素的 flags 值为 WQ_FLAG_EXCLUSIVE即为1表示此进程是高优先级进程。

等待事件

wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

等待第一个参数 queue 作为等待队列头的等待队列被唤醒,而且第二个参数 condition 必须满足否则阻塞。wait_event() 和 wait_event_interruptible() 的区别在于后者可以被信号打断而前者不能。加上_timeout 后的宏意味着阻塞等待的超时时间,以 jiffy 为单位,在第三个参数的 timeout 到达时,不论 condition 是否满足,均返回。

唤醒队列

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

上述操作会唤醒以 queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。

wake_up() 应 与 wait_event() 或 wait_event_timeout() 成对使用,而 wake_up_interruptible() 则应与 wait_event_interruptible() 或 wait_event_interruptible_timeout() 成对使用。wake_up() 可唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的进程,而 wake_up_interruptible() 只能唤醒处于 TASK_INTERRUPTIBLE 的进程。

在等待队列上睡眠

sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);

sleep_on() 函数的作用就是将目前进程的状态置成 TASK_UNINTERRUPTIBLE并定义一个等待队列之后把它附属到等待队列头 q直到资源可获得q 引导的等待队列被唤醒。interruptible_sleep_on() 与 sleep_on() 函数类似,其作用是将目前进程的状态置成 TASK_INTERRUPTIBLE并定义一个等待队列之后把它附属到等待队列头 q直到资源可获得q 引导的等待队列被唤醒或者进程收到信号。sleep_on() 函数应该与 wake_up() 成对使用interruptible_sleep_on() 应该与 wake_up_interruptible()成对使用。

sleep_on() 及 interruptible_sleep_on() 与 wait_event() 和 wait_event_interruptible() 的区别在于sleep_on() 和 interruptible_sleep_on() 不需要 condition 参数,只有调用 wake_up() 或 wake_up_interruptible() 就唤醒队列上的所有等待。

轮询操作

轮询的概念与作用

在用户程序中select() 和 poll() 也是与设备阻塞与非阻塞访问息息相关的论题。使用非阻塞 I/O 的应用程序通常会使用 select()和 poll()系统调用查询是否可对设备进行无阻塞的访问。select() 和 poll() 系统调用最终会引发设备驱动中的 poll() 函数被执行,在 2.5.45 内核中还引入了 epoll(),即扩展的 poll()。

select() 和 poll() 系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。

应用程序中的轮询

应用程序中最广泛用到的是 BSD UNIX 中引入的 select() 系统调用,其原型如下:

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中 readfds、writefds、exceptfds 分别是被 select() 监视的读、写和异常处理的文件描述符集合numfds 的值是需要检查的号码最高的文件描述符加 1。timeout 参数是一个指向 struct timeval 类型的指针,它可以使 select()在等待 timeout 时间后若没有文件描述符准备好则返回。struct timeval 结构如下:

struct timeval
{
    int tv_sec;     // 秒
    int tv_usec;    // 微妙
};

下列操作用来设置、清除、判断文件描述符集合:

// 清除一个文件描述符集
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)

应用程序 Select 示例

下面程序用于监控标准输入输出:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LEN 1024

int main()
{
    int fds[1];     // 只监测标准输入与输出这两个文件描述符
    int std_in = 0;
    int fds_max = 1;
    fd_set reads;
    struct timeval timeout;

    fds[0] = std_in;
    while(1)
    {
        FD_ZERO(&reads);
        FD_SET(std_in, &reads);     // 标准输入关注的是读事件

        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        switch(select(fds_max+1, &reads, NULL, NULL, &timeout))
        {
            case 0:
                printf("select time out ......\n");
                break;
            case -1:
                perror("select");
                break;
            default:
                if(FD_ISSET(fds[0], &reads))    // 可以从标准输入中读
                {
                    char buf[LEN];
                    memset(buf, '\0', LEN);
                    fgets(buf, LEN, stdin);
                    printf("echo: %s", buf);
                    if(strncmp(buf, "quit", 4) == 0)
                    {
                        exit(0);
                    }
                }
                break;
        }
    }
}

设备驱动中的轮询编程

设备驱动中 poll() 函数的原型如下:

unsigned int(*poll)(struct file * filp, struct poll_table* wait);

第一个参数为 file 结构体指针,第二个参数为轮询表指针。这个函数应该进行以下两项工作:

  • 对可能引起设备文件状态变化的等待队列调用 poll_wait()函数,将对应的等待队列头添加到 poll_table。
  • 返回表示是否能对设备进行无阻塞读、写访问的掩码。

用于向 poll_table 注册等待队列的 poll_wait() 函数的原型如下:

void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

poll_wait() 函数并不会引起阻塞,其所做的工作是把当前进程添加到 wait 参数指定的等待列表poll_table中。

驱动程序 poll() 函数应该返回设备资源的可获取状态,即 POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL 等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如 POLLIN定义为 0x0001意味着设备可以无阻塞地读POLLOUT定义为 0x0004意味着设备可以无阻塞地写。

设备驱动轮询示例

#include <linux/poll.h>

struct demo_dev
{
    struct cdev cdev;
    struct device *dev;
    struct semaphore demo_sem;
    unsigned int available;
    // This is a test.
    char demo_text[DEMO_DATA_SIZE];

    wait_queue_head_t r_wait;   // 阻塞读用的等待队列头
    wait_queue_head_t w_wait;   // 阻塞写用的等待队列头
};

static unsigned int demo_poll(struct file *filp, struct poll_table_struct* wait) {
    struct demo_dev *devp = filp->private_data;
    unsigned int mask = 0;

    down(&devp->demo_sem);
    poll_wait(filp, &devp->r_wait, wait);
    poll_wait(filp, &devp->w_wait, wait);

    if(0!=devp->available) {
        mask |= POLLIN | POLLRDNORM;    // 数据可读|普通数据可读
    }
    if(0!=(DEMO_DATA_SIZE-devp->available)) {
        mask |= POLLOUT | POLLWRNORM;   // 数据可写|普通数据可写
    }

    up(&devp->demo_sem);
    return mask;
}

static ssize_t demo_read(struct file *filp, char __user *buffer, size_t count, loff_t *position)
{
    struct demo_dev *devp = filp->private_data;
    loff_t p;
    ssize_t ret = 0;

    down(&devp->demo_sem);
    p = *position;

    // This is a test.
    // 分析和获取有效的读长度
    if(DEMO_DATA_SIZE<=p)   // 要读的偏移位置越界
        return 0;           // End of a file
    if(DEMO_DATA_SIZE<(count+p))    // 要读的字节数太大
        count = DEMO_DATA_SIZE-p;

    if(copy_to_user((void*)buffer, &devp->demo_text[p], count)) {
        up(&devp->demo_sem);
        ret = -EFAULT;
    }
    else
    {
        devp->available -= count;
        *position += count;

        wake_up_interruptible(&devp->w_wait);
        up(&devp->demo_sem);
        ret = count;
    }
    return ret;
}

static ssize_t demo_write(struct file *filp, const char __user *buffer, size_t count, loff_t *position)
{
    struct demo_dev *devp = filp->private_data;
    loff_t p;
    ssize_t ret = 0;

    down(&devp->demo_sem);
    p = *position;
    // This is a test.
    // 分析和获取有效的写长度
    if(DEMO_DATA_SIZE<=p)   // 要读的偏移位置越界
        return 0;           // End of a file
    if(DEMO_DATA_SIZE<(count+p))    // 要读的字节数太大
        count = DEMO_DATA_SIZE-p;

    if(copy_from_user(&devp->demo_text[p], (void*)buffer, count)) {
        up(&devp->demo_sem);
        ret = -EFAULT;
    }
    else {
        devp->available += count;
        *position += count;

        wake_up_interruptible(&devp->r_wait);
        up(&devp->demo_sem);
        ret = count;
    }

    return ret;
}

static struct file_operations demo_fops = {
    .owner          = THIS_MODULE,
    .open           = demo_open,
    .release        = demo_release,
    .llseek         = demo_llseek,
    .poll           = demo_poll,
    .read           = demo_read,
    .write          = demo_write,
    .unlocked_ioctl = demo_ioctl,
    .mmap           = demo_mmap
};

static int demo_setup_cdev(struct demo_dev *devp, int index)
{
    char name[16];
    int err, devno = MKDEV(demo_major, index);

    cdev_init(&devp->cdev, &demo_fops);
    devp->cdev.owner = THIS_MODULE;
    err = cdev_add(&devp->cdev, devno, 1);
    if(err)
    {
        printk(KERN_ERR "demo add cdev:%d error:%d.\r\n", index, err);
        goto out_cdev;
    }

    // 创建设备节点
    memset(name, 0, 16);
    sprintf(name, DEMO_MODULE_NAME"%d", index);
    printk(KERN_INFO "demo new dev name:%s", name);
    devp->dev = device_create(class, NULL, devp->cdev.dev, NULL, name);
    // This is a test.
    devp->demo_text[DEMO_DATA_SIZE-2] = '\n';
    devp->demo_text[DEMO_DATA_SIZE-1] = 0;
    sprintf(devp->demo_text, "%d", index);

    sema_init(&devp->demo_sem, 1);
    devp->available = 0;
    init_waitqueue_head(&devp->r_wait);
    init_waitqueue_head(&devp->w_wait);
    return 0;
out_cdev:
    cdev_del(&devp->cdev);
    kfree(devp);
    return err;
}

上述代码展示了一个 poll() 函数功能以及具体驱动的实现细节。利用这样的框架,我们可以写出类似驱动的 poll() 功能。其中比较关键的函数 poll_wait() 比较具有迷惑性,虽然名字中带有 wait但这个函数本身并不会真正的阻塞因此整个 poll 调用中,不包含任何等待条件满足的程序,这个框架代码开始变得很难理解。想要清楚为何这样编写,就需要了解 Linux 系统 poll 功能实现的机制。

Linux 内核 poll 实现机制

从应用程序调用 poll() 函数开始,直到调用 demo_poll 函数为止,将期间的主要的内容及函数调用关系罗列如下:

app: poll
      |
drv:sys_poll
      |
      +- do_sys_poll(struct pollfd __user * ufds, unsigned int nfds, struct timespec * end_time)
        |
        +- poll_initwait(&table);    // 实际效果:令函数指针 table.pt.qproc = __pollwait这个函数指针最终会传递给 poll_wait() 函数调用中的 wait->qproc
        |
        +- do_poll(nfds, head, &table, end_time);
        |
        +- for ( ; ; )
        {
            for (; pfd!=pfd_end; pfd++) {     // 可以监测多个驱动设备所产生的事件
                if (do_pollfd(pfd, pt)) {
                    |
                    +-  mask = f.file->f.f_op->poll(f.file, pwait);  // 实际效果:执行驱动中的 demo_poll(file, pwait)
                                |
                                +-  poll_wait(filp, &devp->r_wait, wait);   // 实际效果:执行 poll_wait(filp, &devp->r_wait, wait),也就是将进程挂接到 devp->r_wait 等待队列下
                                |
                                +-  mask赋值; return mask;   // 返回事件类型

                        pollfd->revents = mask; // 将实际事件类型返回
                        count++; pt = NULL;
                }
            }
            if (count || timed_out) // 如果有事件发生,或者超时,则跳出 poll
                break;
            if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))    // 如果没有事件发生,那么陷入休眠状态
                 |
                 +- schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
                timed_out = 1;
        }

由此可见demo_poll() 函数,是系统在执行 sys_poll() 过程中的一个调用,调用的目的是“将进程挂接到等待队列下”和“返回事件类型 mask”。当已经发生了请求事件那么通过标记 mask 非 0则 if(do_pollfd(pfd, pt)) 判断为真,令 count++,从而可以直接令 poll() 函数成功返回。如果还没有发生请求的事件,那么 mask 被标记为 0进程将通过函数 poll_schedule_timeout() 陷入休眠状态,直到定时器到时或者等待队列被触发。因为之前已经在驱动中通过 poll_wait() 将进程挂接到等待队列下,所以一旦发生了请求的事件,则进程将被唤醒,重新执行 demo_poll(),而显然此时能够成功返回。