linux设备驱动学习(八)—阻塞式I/O与休眠

阻塞型I/O与休眠

当一个进程被置入休眠时,他会被标记为一种特殊的状态并从调度器的运行队列调走(将该进程加入到等待队列中,等待唤醒)。休眠中的进程会被搁置在一边,等待唤醒。

要想安全的进行休眠需要注意:

1.永远不要在原子上下文中进入休眠.原子上下文是指:在执行多个步骤时,不能有任何的并发访问。对休眠来说,我们的驱动程序不能拥有自旋锁、seqlock或者RCU锁时休眠。如果我们已经禁止了中断,也不能休眠。但在拥有信号量时允许休眠。如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,一次拥有信号量而休眠的代码必须很短,并且还要确保拥有信号量并不会阻塞最终会唤醒我们的进程。

2.每当进程被唤醒,都必须重新检查等待条件来确保正确的响应。

为了确保唤醒发生,并清楚地知道对每个休眠而言哪些事件序列会结束休眠,为此需要维护一个等待队列的数据结构

linux中一个等待队列通过一个等待队列头来管理,等待队列头是一个wait_queue_head_t结构体,定义在<linux/wait.h>

struct __wait_queue_head{

spinlock_t lock;

struct list_head task_list;

}

typedef struct __wait_queue_head wait_queue_head_t;

它包含一个自旋锁和一个链表。这个链表是一个等待队列入口,它被声明做 wait_queue_t。wait_queue_head_t包含关于睡眠进程的信息和它想怎样被唤醒。

静态初始化:DECLEAR_WAIT_QUEUE_HEAD(name);

动态初始化:

wait_queue_head_t my_queue;

init_waitqueue_head(&my_queue);

简单休眠方法是wait_event的宏,又如下实现方式:

wait_event(queue,condition);

wait_event_interruptible(queue,condition);

wait_event_timeout(queue,condition,timeout);

wait_event_interruptible_timeout(queue,condition,timeout); //这里注意休眠队列是通过值传递完成的。

queue为等待队列头,注意,它是通过值进行传递的,而不是通过指针。condition是一个布尔值,上面的宏在休眠前后对该表达式求值,在条件为真前,进程会保持休眠。

与down_interruptible(&my_sem)一样interruptible是我们常用的版本。

wait_event_interrupt(queue,condition)返回非零值,表示该休眠被某个信号中断。

最后两个表示在给定的时间内休眠,timeout到期时,都返回零,而无论condition取何值。

void wake_up(wait_queue_head_t *queue);//这里注意唤醒进程是通过指针来传递的

void wake_up_interruptible(wait_queue_head_t *queue);

代码示例:

static DECLEAR_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_interuptible(wq,flag!=0);

flag = 0;

printk(KERN_DEBUG “awken process %i (%s)../n”,current->pid,current->comm);

return 0; //EOF

}

ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos)

{

printk(KERN_DEBUG “process %i (%s) going to awken the reader../n”,current->pid,current->comm);

flag =1; // 先设置唤醒条件条件

wake_up_interruptible(wq);

return count;

}

这里需要注意个问题,如果有两个sleepy_read同时等待wake_up_interruptible,在第一个read被唤醒时会将flag置为零,因此可能会认为第二个read会立即进入休眠状态。但是要注意wake_up_interruptible(wq)会唤醒所有等待的进程,而在重置flag之前,两个read进程都完全有可能注意到这个标志的变化。所以,如果要想确保只有一个进程看见这个费零值,则必须以原子的方式进行检查。

阻塞和非阻塞操作:

全功能的 read 和 write 方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再 <linux/fcntl.h> ,被 <linux/fs.h>自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。O_NONBLOCK 标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠。

如果指定了O_NOBLOCK,read和write会有所变化,如果没有数据就绪时调用read或者在缓冲区没空间是调用write,则该调用简单的返回-EAGAIN。

这里说明在非阻塞操作会立即返回,从而使得应用程序可以查询数据的变化。例如在应用程序中,select调用可以对文件集进行侦测,底层调用poll来完成状态的查询。

另外很容易把非阻塞的返回误认为是EOF,所以必须始终检查errno的值。

O_NOBLOCK在open调用中也很有用,它应用于在open调用可能会阻塞很长时间的场合。例如有的设备可能会初始化很长的时间,这是就可以选择在open方法中O_NOBLOCK,如果该标志被置位,则在设备开始初始化后会立刻返回-EAGAIN。

只有read、open、open文件操作受非阻塞操作的影响。

这里列出write和read默认方向的问题:

scull_p_read(struct file *filp,char __user *buf,size_t count,loff_t *pos);

scull_p_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos);

两者的角度都是从用户程序上看的,read表示从缓冲区读,write则相反。

这里给出一段实例代码:

static ssize_t scull_p_read(struct file *filp,char __user *buf,size_t count ,loff_t *pos)

{

struct scull_pipe *dev = filp->private;

iif(down_interruprible(&dev->sem)) //如果操作被中断,会返回一个非零值,而调用者不会拥有该信号。

return -ERESTARTSYS;

while(dev->rp == dev->wp){ //表示无数据可读,对结构内成员进行操作在持有锁的情况下,注意while始终在拥有信号的前提下对缓冲区进行检查。如果有数据可读直接返回给用户,循环被跳过。相反如果为空则必须休眠。

up(&dev->sem);

在对设备内部的读写指针进行检查后,释放信号量,注意信号量必须在读进程进入休眠之前释放,否则任何写入者都没有机会来唤醒,原因在于读写是用信号量来进行互斥的,如果读进程在拥有信号的量情况下休眠,则写进程永远得不到这个锁。

if(filp->f_flags & O_NONBLOCK)

return -EAGAIN;

if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp)))

return -ERESTARTSYS;

快速检查用户请求的是否是非阻塞I/O,如果是,则返回,否则将读进程休眠。这里要明确一点对于interruptible族的调用来说总是要时刻检查其返回值,如down_interuptible、wait_event_interruptible等。如果非零则表示操作被中断,同时驱动程序返回ERESTARTSYS

if(down_interruptible(&dev->sem))

return -ERESTARTSYS;

}

当wait_event_interruptible这个函数返回时,说明其他人唤醒了等待队列,但不知道具体是哪个情况(进程接受到一个信号或者缓冲区有数据可读)。if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp))这条IF语句确保了对信号正确的响应,该信号可能用来唤醒进程的。如果信号没有被阻塞,正确的动作时让内核的上层去处理这个事件。为此驱动程序返回给调用者ERESTARTSYS,这个值由虚拟文件系统层(VFS)内部使用。它或者重启系统调用,或者给用户空间返回-EINTR;

就算不是因为信号而被唤醒,我们仍然无法确认是否有数据可获得。其他人可能也在等待数据,而且可能赢得竞争并拿走了数据。因此我们必须重新获得信号量,来对缓冲区进行检查。当从while循环推出时,我们持有信号量并且缓冲区包含有可使用的资源。

if(dev->wp > dev->rp)

count = min(count,(size_t)(dev->wp – dev->rp));

else

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设备驱动学习(八)—阻塞式I/O与休眠

相关文章:

你感兴趣的文章:

标签云: