The Driver of Block Character Device

具有阻塞操作的字符设备驱动

http://lzz5235.github.com/2014/10/13/the-driver-of-character-device.html 在上面这篇文章里面,我们做的是一个简单的非阻塞操作的字符设备,也就是说对于这个设备的操作要么放弃,要么不停的轮询,直到操作可以进行下去。

而支持阻塞设备的字符操作,我们可以实现读操作或者写操作的睡眠。这就是我们要完成的操作。

关于程序的框架,与之前的其实是类似的,比如我们在全局设定一个等待队列头。


#define MYCDEV_SIZE 100
#define DEVICE_NAME "lzz_block_cdev"
 
static char globalmem[MYCDEV_SIZE];
static wait_queue_head_t rdwait;
static wait_queue_head_t wrwait;
static struct semaphore mutex;
 
static int len;
ssize_t myblock_read(struct file*,char*,size_t count,loff_t*);
ssize_t myblock_write(struct file*,char*,size_t count,loff_t*);
ssize_t mycdev_open(struct inode *inode, struct file *fp);
ssize_t mycdev_release(struct inode *inode, struct file *fp);

在模块初始化中,首先要初始化mutex,rdwait,wrwait。


static int __init mycdev_init(void)
{
    int ret;
 
    printk("myblock module is working..\n");
 
    ret=register_chrdev(MYCDEV_MAJOR,DEVICE_NAME,&fops);
    if(ret<0)
    {
        printk("register failed..\n");
        return 0;
    }
    else
    {
        printk("register success..\n");
    }
    sema_init(&mutex,1);
    init_waitqueue_head(&rdwait);
    init_waitqueue_head(&wrwait);
 
    return 0;
}

先创建一个代表当前进程的等待队列结点wait,并把它加入到读等待队列当中。

当共享数据区的数据长度为0时,就阻塞该进程。因此,在循环中,首先将当前进程的状态设置TASK_INTERRUPTIBLE。

然后利用schedule函数进行重新调度,此时,读进程才会真正的睡眠,直至被写进程唤醒。在睡眠途中,如果用户给读进程发送了信号,那么也会唤醒睡眠的进程。

当共享数据区有数据时,会将count字节的数据拷贝到用户空间,并且唤醒正在睡眠的写进程。当上述工作完成后,会将当前进程从读等待队列中移除,并且将当前进程的状态设置为TASK_RUNNING。

关于从全局缓冲区移出已读数据,这里要特别说明一下。这里利用了memcpy函数将以(globalmem+count)开始的(len-count)字节的数据移动到缓冲区最开始的地方。

###signal_pending(current)检查当前进程是否有信号处理,若要处理就返回非0!然后这个函数退出,插入到等待队列,等待下次重新开始执行!


ssize_t myblock_read(struct file*fp,char*buf,size_t count,loff_t*offp)
{
    int ret;
    DECLARE_WAITQUEUE(wait,current);
 
    down(&mutex);
    add_wait_queue(&rdwait,&wait);
 
    while(len==0)
    {
        __set_current_state(TASK_INTERRUPTIBLE);
        up(&mutex);
        schedule();
        if(signal_pending(current))
        {
            ret=-1;
            goto signal_out;
        }
 
        down(&mutex);
    }
 
    if(count>len)
    {
        count=len;
    }
 
    if(copy_to_user(buf,globalmem,count)==0)
    {
        memcpy(globalmem,globalmem+count,len-count);
        len-=count;
        printk("read %d bytes\n",count);
        wake_up_interruptible(&wrwait);
        ret=count;
    }
    else
    {
        ret=-1;
        goto copy_err_out;
    }
 
copy_err_out:up(&mutex);
signal_out:remove_wait_queue(&rdwait,&wait);
 
    set_current_state(TASK_RUNNING);
    return ret;
}

写函数的控制流程大致与读函数相同,只不过对应的等待队列是写等待队列。 唤醒后如何执行。无论因哪种方式而睡眠,当读进程被唤醒后,均顺序执行接下来的代码。


ssize_t myblock_write(struct file*fp,char*buf,size_t count,loff_t*offp)
{
    int ret;
    DECLARE_WAITQUEUE(wait,current);
 
    down(&mutex);
    add_wait_queue(&wrwait,&wait);
 
    while(len==MYCDEV_SIZE)
    {
        __set_current_state(TASK_INTERRUPTIBLE);
        up(&mutex);
        schedule();
        if(signal_pending(current))
        {
            ret=-1;
            goto signal_out;
        }
 
            down(&mutex);
    }
    if(count>(MYCDEV_SIZE-len))
    {
        count=MYCDEV_SIZE-len;
    }
 
    if(copy_from_user(globalmem+len,buf,count)==0)
    {
        len=len+count;
        printk("written %d bytes\n",count);
        wake_up_interruptible(&rdwait);
        ret=count;
    }
    else
    {
        ret=-1;
        goto COPY_ERR_OUT;
    }
 
signal_out:up(&mutex);
COPY_ERR_OUT:remove_wait_queue(&wrwait,&wait);
    set_current_state(TASK_RUNNING);
 
    return ret;
}

读写函数down操作和add_wait_queue操作交换,可能会造成死锁。

up操作和remove_wait_queue操作交换。如果读进程从内核空间向用户空间拷贝数据失败时,就会从up往后执行。

因为读进程是在获得信号量后才拷贝数据的,因此必须先释放信号量,再将读进程对应的等待队列项移出读等待队列。而当读进程因信号而被唤醒时,则直接跳转到remove_wait_queue操作,并往后执行(仅仅只是睡眠了,wake_up后继续向后执行)。

此时读进程并没有获得信号量,因此只需要移出队列操作即可。如果交换上述两个操作,读进程移出等待队列时还未释放互斥信号量,那么写进程就不能写。而当读进程因信号而唤醒时,读进程并没有获得信号量,却还要释放信号量。

####对于goto操作:就是从Label以后,一直往后执行,并不是执行一条语句而已。

使用模块的方式,上篇博文说的很清楚。 使用的时候: