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

October 13th, 2014 by JasonLe's Tech Leave a reply »

http://www.lizhaozhong.info/archives/1062

在上面这篇文章里面,我们做的是一个简单的非阻塞操作的字符设备,也就是说对于这个设备的操作要么放弃,要么不停的轮询,直到操作可以进行下去。

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

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

#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)字节的数据移动到缓冲区最开始的地方。

在调用schedule函数退出CPU后,下次唤醒后进入运行时将从schedule语句的下一条语句开始,即if (signal_pending(current)) 语句。

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后继续向后执行)。

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

设想一种情况:

while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

当schedule()重新调度该程序,从if()开始执行遭遇用户的Ctrl+C ,就会跳入signal_out。如果没有用户信号,因为在schedule()前已经up()了,所以接下来要拷贝数据,使用down()

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

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

  • 终端输入:cat /dev/blockcdev&;即从字符设备文件中读数据,并让这个读进程在后台执行,可通过ps命令查看到这个进程;

  • 中断继续输入:echo ‘I like eating..’ > /dev/blockcdev;即向字符设备文件中写入数据;