Read方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在<asm/uaccess.h>中定义的:
unsigned long copy_to_user(void __user *to,
const void *from,
unsigned long count);
unsigned long copy_from_user(void *to,
const void __user *from,
unsigned long count);
那么当驱动程序无法立即满足请求,该如何响应? 比如:当数据不可用时调用read时怎么办。解决问题:驱动程序应该(默认)该阻塞进程,将其置入休眠状态直到请求可继续。
在这里就要插入阻塞和非阻塞模型的概念。阻塞操作是指在执行I/O操作时,若不能获得资源,则进程挂起直到满足可操作的条件在进行操作。非阻塞操作是指在执行I/O操作时,如果设备没有准备好,并不挂起,立即返回。显示的非阻塞I/O由file->f_flags中的O_NONBLOCK
决定。
那么如何利用阻塞操作实现 read方法 。不管什么先贴代码先
static ssize_t v4l_cam_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
unsigned long flags;
int retval = 0;
struct s3c2440_camif *dev = file->private_data;
DPRINTK("%s():\n", __FUNCTION__);
if(file->f_flags&O_NONBLOCK)
{
if(down_trylock(&dev->change))
return -EAGAIN;
}
else
{
if(down_interruptible(&dev->change))
return -EINTR; }
local_irq_save(flags);
if(!dev->rdy)
{
if(file->f_flags&O_NONBLOCK)
retval = -EAGAIN;
else
{
if(wait_event_interruptible(dev->wait,dev->rdy))
retval= -ERESTARTSYS;
/*interruptible_sleep_on(&dev->wait);
if(!dev->rdy)
retval = -EINTR;
*/
}
}
local_irq_restore(flags);
if(!retval)
{ //now if retval==0, dev->rdy must be 1
dev->rdy = 0; //clear ready flag
retval = min(count, dev->size);
if(copy_to_user(buf, (void *)camif_yuv_buf, retval)) //can copy 0 byte
retval = -EFAULT;
}
up(&dev->change);
return retval;
}
首先可以看出在file->f_flags中判断是否是非阻塞操作。然后不管什么先获取内核信号量down_trylock(&dev->change)和down_interruptible(&dev->change)就是在获取内核信号量的操作。说到这里要扯一扯什么是信号量,以及信号量怎么操作的问题。
信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)
所拥有。信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为 0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。
在Linux 下提供两种信号量:
(1) 内核信号量,由内核控制路径使用
(2) 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量和 SYSTEM
V 信号量。
POSIX 信号量又分为有名信号量和无名信号量。
有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。无名信号量,其值保存在内存中。
在上文中我们提到了内核信号量,内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。 然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。 只有在资源被释放时,进程才再次变为可运行。只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。内核信号量是
struct semaphore 类型的对象,它在<asm/semaphore.h>中定义:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
}
count:相当于信号量的值,大于 0,资源空闲;等于 0,资源忙,但没有进程等待这
个保护的资源;小于 0,资源不可用,并至少有一个进程等待资源。
wait:存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。
sleepers:存放一个标志,表示是否有一些进程在信号量上睡眠。
那么我们为什么要在read 方法中使用内核信号量呢?原因在于在驱动程序中,当多个线程同时访问相同的资源时(驱动中的全局变量时一种典型的共享资源),可能会引发“竞态“,因此我们必须对共享资源进行并发控制。Linux 内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
明白了以上的问题我们就可以继续往下解析 s3c2440中camera 代码中的read 方法。
当我们获取了对资源的保护后,就调用local_irq_save(flags);禁止本地中断。在 多CPU下这个函数可能会失效。。。。(这个很纠结。但是由于我们使用的是单CPU所以,使用local_irq_save(flags);既可以禁止本地中断。在这里重要的是禁止dam中断。
if(!dev->rdy)
{
if(file->f_flags&O_NONBLOCK)
retval = -EAGAIN;
else
{
if(wait_event_interruptible(dev->wait,dev->rdy))
retval= -ERESTARTSYS;
}
}
接下去这部分代码就是根据自定义设备结构体中的标志位rdy判断是否有数据喽。。(rdy在DMA中断被置位。这也就是为什么在read方法中要禁止本地中断的原因)。上文的代码表示没有数据的情况下,非阻塞操作立刻返回,同时返回错误代码EAGAIN。而阻塞操作就将进程加入等待队列,等满足条件后来取数据。
local_irq_restore(flags);
if(!retval)
{ //now if retval==0, dev->rdy must be 1
dev->rdy = 0; //clear ready flag
retval = min(count, dev->size);
if(copy_to_user(buf, (void *)camif_yuv_buf, retval)) //can copy 0 byte
retval = -EFAULT;
}
up(&dev->change);
如果有数据,则是先释放本地中断,然后利用linux内核函数copy_to_user将数据拷贝给用户层。这样就完成了数据的读操作。最后一步up(&dev->change);这个是释放信号量。。至于为什么没有看到唤醒等待队列中的进程的函数呢?因为这个在C通道中断函数中做了。会有专门的篇幅介绍C通道中断函数的