最后, 我们看一个实现了阻塞 I/O 的真实驱动方法的例子. 这个例子来自 scullpipe 驱 动; 它是 scull 的一个特殊形式, 实现了一个象管道的设备.
在驱动中, 一个阻塞在读调用上的进程被唤醒, 当数据到达时; 常常地硬件发出一个中断 来指示这样一个事件, 并且驱动唤醒等待的进程作为处理这个中断的一部分. scullpipe 驱动不同, 以至于它可运行而不需要任何特殊的硬件或者一个中断处理. 我们选择来使用
另一个进程来产生数据并唤醒读进程; 类似地, 读进程被用来唤醒正在等待缓冲空间可用 的写者进程.
这个设备驱动使用一个设备结构, 它包含 2 个等待队列和一个缓冲. 缓冲大小是以常用 的方法可配置的(在编译时间, 加载时间, 或者运行时间).
struct scull_pipe
{
wait_queue_head_t inq, outq; /* read and write queues */ char *buffer, *end; /* begin of buf, end of buf */
int buffersize; /* used in pointer arithmetic */ char *rp, *wp; /* where to read, where to write */
int nreaders, nwriters; /* number of openings for r/w */
struct fasync_struct *async_queue; /* asynchronous readers */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */
};
读实现既管理阻塞也管理非阻塞输入, 看来如此:
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)
{ /* nothing to read */
up(&dev->sem); /* release the lock */ if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
PDEBUG(""%s" reading: going to sleep ", current->comm);
if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp))) return -ERESTARTSYS; /* signal: tell the fs layer to
handle it */ /* otherwise loop, but first reacquire the lock */
if (down_interruptible(&dev->sem)) return -ERESTARTSYS;
}
/* ok, data is there, return something */
if (dev->wp > dev->rp)
count = min(count, (size_t)(dev->wp - dev->rp));
else /* the write pointer has wrapped, return data up to 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; /* wrapped */ up (&dev->sem);
/* finally, awake any writers and return */ wake_up_interruptible(&dev->outq);
PDEBUG(""%s" did read %li bytes ",current->comm, (long)count);
return count;
}
如同你可见的, 我们在代码中留有一些 PDEBUG 语句. 当你编译这个驱动, 你可使能消息 机制来易于跟随不同进程间的交互.
让我们仔细看看 scull_p_read 如何处理对数据的等待. 这个 while 循环在持有设备旗 标下测试这个缓冲. 如果有数据在那里, 我们知道我们可立刻返回给用户, 不必睡眠, 因 此整个循环被跳过. 相反, 如果这个缓冲是空的, 我们必须睡眠. 但是在我们可做这个之 前, 我们必须丢掉设备旗标; 如果我们要持有它而睡眠, 就不会有写者有机会唤醒我们. 一旦这个确保被丢掉, 我们做一个快速检查来看是否用户已请求非阻塞 I/O, 并且如果是 这样就返回. 否则, 是时间调用 wait_event_interruptible.
一旦我们过了这个调用, 某些东东已经唤醒了我们, 但是我们不知道是什么. 一个可能是 进程接收到了一个信号. 包含 wait_event_interruptible 调用的这个 if 语句检查这种 情况. 这个语句保证了正确的和被期望的对信号的反应, 它可能负责唤醒这个进程(因为 我们处于一个可中断的睡眠). 如果一个信号已经到达并且它没有被这个进程阻塞, 正确 的做法是让内核的上层处理这个事件. 到此, 这个驱动返回 -ERESTARTSYS 到调用者; 这 个值被虚拟文件系统(VFS)在内部使用, 它或者重启系统调用或者返回 -EINTR 给用户空 间. 我们使用相同类型的检查来处理信号, 给每个读和写实现.
但是, 即便没有一个信号, 我们还是不确切知道有数据在那里为获取. 其他人也可能已经 在等待数据, 并且它们可能赢得竞争并且首先得到数据. 因此我们必须再次获取设备旗标; 只有这时我们才可以测试读缓冲(在 while 循环中)并且真正知道我们可以返回缓冲中的 数据给用户. 全部这个代码的最终结果是, 当我们从 while 循环中退出时, 我们知道旗 标被获得并且缓冲中有数据我们可以用.
仅仅为了完整, 我们要注意, scull_p_read 可以在另一个地方睡眠, 在我们获得设备旗 标之后: 对 copy_to_user 的调用. 如果 scull 当在内核和用户空间之间拷贝数据时睡 眠, 它在持有设备旗标中睡眠. 在这种情况下持有旗标是合理的因为它不能死锁系统(我 们知道内核将进行拷贝到用户空间并且在不加锁进程中的同一个旗标下唤醒我们), 并且 因为重要的是设备内存数组在驱动睡眠时不改变.