高级字符驱动程序操作——阻塞型I/O

阻塞型I/O

前面已经介绍了驱动程序的read和write方法。现在讨论另一个问题:如果驱动程序无法立即满足请求,该如何响应?当数据不可用时,用户可能调用read;或者进程试图写入数据,但因为输出缓冲区已满,设备还未准备好接受数据。调用进程通常不会关心这类问题,它们只会简单的调用read和write。因此在这种情况下,我们的驱动程序应该(默认)阻塞该进程,将其置入休眠状态直到请求可继续。

这一节说明了如果使进程进入休眠状态并在将来唤醒。先来看一些新概念。

休眠的简单介绍

“休眠(sleep)”对进程来讲就是它被标记为一种特殊状态并从调度器的运行队列中移走。直到某些情况下修改了这个状态,进程才会回到运行状态。

为了将进程以一种安全的方式进入休眠,我们需要牢记以下两条规则:

  1. 永远不要在原子上下文中进入休眠,如果上下文中禁止了中断也不能休眠。
  2. 当我们被唤醒时,必须检查以确保我们等待的条件为真。

除非我们知道有其他人会在其他地方唤醒我们,否则不能休眠。完成唤醒任务的代码还必须能够找到我们的进程,这样才能唤醒休眠的进程。为确保唤醒发生,我们必须整体理解我们的代码,并清楚地知道对每个休眠而言哪些事件序列会结束休眠。能够找到休眠的进程意味着,我们需要维护一个称为等待队列的数据结构。等待队列就是一个进程链表,其中包含了等待某个特定事件的所有进程。

在Linux中,一个等待队列通过一个“等待队列头”来管理,等待队列头是一个类型为wait_queue_head_t的结构体,定义在<linux/wait.h>中。可通过如下静态方法定义并初始化一个等待队列头:

DECLARE_WAIT_QUEUE_HEAD(name);

或者通过使用动态方法:

wait_queue_head_t my_queue;

init_waitqueue_head(&my_queue);

在Linux中,等待队列是实现同步机制的重要数据结构,如信号量的实现

struct semaphore {
    atomic_t count;
    int sleepers;
    wait_queue_head_t wait;
};

简单休眠

当进程休眠时,它将期待某个条件会在将来成为真。我们前面提到,当一个被唤醒时时,它会再次检查它所等待的条件为真。linux内核中最简单的休眠方式是如下宏:

wait_event(queue, condition)    非中断休眠
wait_event_interruptible(queue, condition)   可以被信号中断,返回非0表示被信号中断,驱动也许要返回-ERESTARTSYS
wait_event_timeout(queue, condition, timeout) 只会等待限定时间,时间到返回0,无论condition是何值
wait_event_interruptible_timeout(queue,conditiion, timeout) 同上

上面的宏在休眠前后都要对表达式求值;在条件为真之前,进程会保持休眠。注意,条件可能会被多次求值,因此对表达式的求值不能带来任何副作用。

下面介绍两个用于唤醒的函数:

void wake_up(wait_queue_head_t *queue); 唤醒queue上的所有进程
void wake_up_interruptible(wait_queue_head_t *queue); 只唤醒那些执行可中断休眠的进程

下面看个简单例子:任何试图从该设备上读取的进程均被置为休眠,只要某个进程向该设备写入,所有休眠的进程就会被唤醒

static DECLARE_WAIT_QUEUE_HEAD(wq);
static int flag = 0;

ssize_t sleepy_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) going to sleep\n"),
        current->pid, current->comm);
    wait_event_interruptible(wq, flag != 0);
    flag = 0;
    printk(KERN_DEBUG "awoken %i (%s) \n", current->pid, current->comm);
    return 0;
}

ssize_t sleepy_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n"),
        current->pid, current->comm);
    flag = 1;
    wake_up_interruptible(&wq);
    return count; /*成功并避免重试*/
}

上面程序中,在重置flag之前,两个休眠进程完全有可能注意到标志为非零。在真实的驱动程序中要避免这种竞态。如果要确保只有一个进程看到非零值,则必须以原子方式进行检查。我们很快就会看到真实的驱动程序如何处理这类问题。

阻塞和非阻塞型操作

 在实现正确的UNIX语义时,有时我们要实现非阻塞的操作,尽管操作不能完整的执行。

有时调用进程会通知我们它不想阻塞,而不管其I/O是否可以继续。显示的的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定的。

如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单地返回-EAGAIN(try it again)。

只有read、write、和open文件操作会受非阻塞标志的影响。

一个阻塞I/O示例

我们通过一个示例来分析实现阻塞I/O的真实驱动方法,这个例子来自scullpipe驱动程序。它是scull实现类似管道设备的特殊形式。

struct scull_pipe {
    wait_queue_head_t inq, outq;  /*读取和写入队列*/
    char *buffer, *end;           /*缓冲区的起始和结束*/
    int buffersize;               /*用于指针计算*/
    char *rp, *wp;                /*读取和写入的位置*/
    int nreaders, nwriters;       /*用于读写打开的数量*/
    struct fasync_struct *async_queue;/*异步读取者*/
    struct semaphore sem;         /*互斥信号量*/
    struct cdev cdev;             /*字符设备结构*/
};

static ssize_t scull_p_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct  scull_pipe *dev = filp->private_data;

    if(down_interruptible(&dev->sem))
        return -ERESTARTSYS;

    while(dev->rp == dev->wp) /*无数据可读*/
    {
        up(&dev->sem);  /*释放锁*/
        if(filp->f_flags & O_NONBLOCK)
            return -EAGAIN;  /*非阻塞直接返回,try again*/
        PDBUG("\"%s\" reading: going to sleep\n", current->comm);
        if(wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
            return -ERESTARTSYS; /*信号,通知fs层做相应处理*/
        /*被写入进程唤醒,但首先应获取锁*/
        if(down_interruptible(&dev->sem))
            return -ERESTARTSYS;
    }
    /*退出while循环,说明拥有信号量,且缓冲区中包含有可使用的数据*/
    if(dev->wp > dev->rp)
        count = min(count, (size_t)(dev->wp - dev->rp));
    else /*写入指针回卷,返回数据直到dev->end*/
        count = min(count, (size_t)(dev->end - dev->rp));
    if(copy_to_user(buf, dev->rp, count)) {
        up(&dev->sem);
        return -EFAULT;
    }
    dev->rp += count;
    if(dev->rp == dev->end)
        dev->rp = dev->buffer; /*回卷*/
    up(&dev->sem);

    /*最后,唤醒所有写入者并返回*/
    wake_up_interruptible(&dev->outq);
    PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
    return count;
}

高级休眠

前面介绍的过的函数可以满足许多驱动程序的休眠需求。但是某些情况下我们需要对Linux的等待队列机制有更深入的理解。复杂的锁定以及性能需求会强制驱动程序使用低层的函数来实现休眠。本小节,我们将讨论一些低层次的细节,以便理解进程在休眠时到底发生了什么事情。

进程如何休眠

1:分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列

2:设备进程的状态,将其标记为休眠

3:放弃处理器

手工休眠

手工休眠其实就是wait_event宏的内部实现,第一个步骤是建立并初始化一个等待队列入口。通常通过宏DEFINE_WAIT(my_wait)完成;下一个步骤是将我们的等待队列入口添加到队列中,并设置进程状态。这两具任务可通过下面宏完成:

void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);

第三个步骤是调用schedule,当然在这之前仍有必要检查一下等待条件是否仍然成立。一旦schedule返回,就到了清理时间了。这可通过下面函数完成:

void finish_wait(wait_queue_head_t *uqeue, wait_queue_t *wait);

下面看看write方法本身:

/*有多少空间被释放*/
static int spacefree(struct scull_pipe *dev)
{
    if(dev->rp = dev->wp)
        return = dev->buffersize -1;
    return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) -1;
}

static ssize_t scull_p_write(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct scull_pipe *dev = filp->private_data;
    int result;

    if(down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    /*确保有空间写入*/
    result = scull_getwritespace(dev, filp);
    if(result) //返回0说明有空间可写
        return result;/*scull_getwritespace会调用up(&dev->sem)*/

    /*有空间可写,接受数据*/
    count = min(count, (size_t)spacefree(dev));
    if(dev->wp >= dev->rp)
        count = min(count, (size_t)(dev->end - dev->wp));/*直到缓冲区尾*/
    else /*写入指针回卷,填充到rp-1*/
        count = min(count, (size_t)(dev->rp - dev->wp -1));
    PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf);
    if(copy_from_user(dev->wp, buf, count)) {
        up(&dev->sem);
        return -EFAULT;
    }
    dev->wp += count;
    if(dev->wp == dev->end)
        dev->wp = dev->buffer; /*回卷*/
    up(&dev->sem);

    /*最后, 唤醒读取者*/
    wake_up_interruptible(&dev->inq);/*阻塞在read()和select()上*/

    /*通知异步读取者,将在后面解释*/
    if(dev->async_queue)
        kell_fasync(&dev->async_queue, SIGIO, POLL_IN);

    PDBUG("\"%s\" did write %li bytes\n", current->comm, (long)count);
    return count;
}

真正处理休眠的代码在下面的函数中,该函数确保新数据有缓冲区可用,并且在必要时休眠直到空间可用。一旦获得空间,即将用户数据写入缓冲区并调整指针,唤醒可能正在等待数据的任何进程。

/*等待有可用写入的空间;调用者必须拥有设备信号量。
  在错误情况下,信号量将在返回前释放。*/
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{
    while(spacefree(dev) == 0) { /* full */
        DEFINE_WAIT(wait);
        
        up(&dev->sem);
        if(filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        PDEBUG("\"%s\" writing: going to sleep\n", current->comm);
        prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);//原子操作加入等待队列和设置状态
        if(spacefree(dev) == 0)//若不做这个检查,在将自己放到等待队列之前,另一进程此时完全清空了缓冲区,则我们将失去唯一被唤醒的机会,从而永远休眠
            schedule();
        finish_wait(&dev->outq, &wait); //设置状态 移出队列
        if(signal_pending(current))
            return -ERESTARTSYS; /*信号,通知fs层做相应处理*/
        if(down_interruptible(&dev->sem))
            return -ERESTARTSYS;
    }
    return 0;
}

scullpipe的read方法使用了wait_event,而write方法使用了prepare_to_wait和finish_wait。通常,我们不应该在一个驱动程序中混合使用这两种方法,但是这里只是希望说明处理休眠的两种方法而已。

独占等待

当某个进程在等待队列上调用wake_up时,所有等待在该队列上的进程都将被置为可运行状态。在大多数情况下这是正确的,但有些情况下,我们可以预先知道只会有一个被唤醒的进程可以获得期望的资源,而其他被唤醒的进程只会再次进入休眠。这些被唤醒进程中的每一个都要获得处理器,为资源(以及锁)竞争,然后又再次进入休眠。如果等待队列中的进程数量非常大,这种“疯狂兽群”行为将严重影响系统性能。

为解决这个问题内核开发者为内核增加了“独占等待”的选项。一个独占等待的行为和通常的休眠类似,但是有以下两点不同:

  1. 等待队列入口设置了WQ_FLAG_EXCLUSIEV标志时,则会被添加到等待队列的尾部,而没有这个标志的入口会被添加到头部。
  2. 在某个等待队列上调用wake_up时,它会在唤醒第一个具有WQ_FLAG_EXCLUSIEVE标志的进程之后停止唤醒其他进程。

最终结果是执行独占等待的进程每次只会被唤醒其中一个(以某种有序的方式),从而不会产生“疯狂兽群”问题。但是,内核第次仍然会唤醒所有非独占等待进程。

存在以下两个条件利用独占是值得考虑的:对某个资源存在严重的竞争,并且唤醒单个进程就能完整消耗该资源。

调用prepare_to_wait_exclusive是将进程置于可中断状态的一种简单方式:

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

注意,使用wait_event及其变种是无法执行独占等待的。

唤醒的相关细节

当一个进程被唤醒时,实际的结果由等待队列入口中的一个函数控制。默认的唤醒函数将进程设置为可运行状态,并且如果进程具有更高的优先级,则会执行一次上下文切换以便切换到该进程。设备驱动程序基本上没有必要提供不同的唤醒函数。

wake_up(wait_queue_head_t *queue);唤醒队列上所有非独占等待的进程以及一个独占等待进程(如果存在)
wake_up_interruptible(wait_queue_head_t *queue);同上,只是它会跳过不可中断休眠的那些进程

wake_up_nr(wait_queue_head_t *queue, int nr);同wake_up,只是它会唤醒nr个独占等待进程,传0表示唤醒所有独占等待进程
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);同上,只是会跳过不可中断进程

wake_up_all(wait_queue_head_t *queue);不管是否执行独占等待都唤醒
wake_up_interruptible_all(wait_queue_head_t *queue);同上,只是跳过不可中断进程

wake_up_interruptible_sync(wait_queue_head_t *queue);被唤醒的进程可能会抢占当前进程,并在wake_up返回前被调度到处理器上。换句话说,对wake_up的调用可能不是原子的。如果wake_up运行在原子上下文(例如仍有锁,或者是一个中断处理例程)中,则重新调度就不会发生。通常这一保护是适当的。如果你不希望在这时被调度出处理器,则可使用wake_up_interruptible的sync变种。这一函数通常在调用者打算强制重新高度的情况下使用,并且在只有很少的工作需要首先完成时更加有效。

除了wake_up_interruptible之外,驱动程序很少调用其他wake_up函数。

旧的历史:sleep_on

void sleep_on(wait_queue_head_t *queue);

void interruptible_sleep_on(wait_queue_head_t *queue);

这两个函数是将当前进程无条件休眠在给定的队列上。但是永远不要使用它们。因为在代码决定休眠及sleep_on真正产生作用之间,总是存在一个窗口,而在窗口期间出现的唤醒将会被丢失。为些调用sleep_on的代码整体上是不安全的。在不远的将来,内核将删除sleep_on及其变种。

 

 

 



 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值