linux驱动(第六课IO方式)

IO模型是APP和DRIVER协同工作方式一种架构。
用来提供驱动处理来自用户进程的IO请求的判据。

当用户进程打开一个FILE时,会关联到某个DEV,同时设置FILE的其他数据成员,例如buf,pos,flags,等等。
从用户的角度,它只需要关注FILE,至于FILE从哪里获取数据,如何获取数据,是由内核负责提供服务的。内核负责解析来自与FILE的数据请求,并调用DRIVER进一步完成IO,内核同时也会传入FILE的句柄,让DRIVER能够进一步了解FILE的相关信息。
也就是说,DRIVER的服务对象,实质上是FILE及其提供的buf。
用户进程打开FILE,这个FILE关联到DEV,同时设置了FLAGS。其中的f_flags就标记了这个FILE的IO方式。
当DRIVER被调用时,内核会传入一个filepointer,这个filp指向用户的FILE。DRIVER根据f_flags来判断,用户需要哪种方式的IO服务。

首先来看看NONBLOCK。
当用户进程请求内核服务时,进程切换CPU模式,在SVC态执行。
内核会解析来自与FILE的数据请求,并调用DRIVER进一步完成IO。
当DRIVER判断出要以非阻塞的方式向FILE提供IO服务时,如果资源不可得,那么DRIVER会立即return。内核也会根据DRIVER返回的状态,对FILE做出进一步的处理,并调度用户进程继续执行,从而将CPU交还给用户代码。
整个过程中,用户进程只是切换了CPU的模式,并没有被阻塞,它始终在RUN队列中。一旦内核返回,它又重新获得CPU。
非阻塞最大的缺点是,需要不断的查询是否能够成功IO,这将霸占CPU时间。
但是它也有自己的优点,在IO并非是关键步骤时,用户进程可以先去做其他的工作,过一段时间后再尝试IO。

再来看看BLOCK。
当用户进程请求内核服务时,进程切换CPU模式为SVC,让内核执行。
内核会解析来自与FILE的数据请求,并调用DRIVER进一步完成IO。
当DRIVER判断出要以阻塞的方式向FILE提供IO服务时,如果资源不可得,那么DRIVER会请求内核服务挂起进程。内核也会根据DRIVER返回的状态,对FILE做出进一步的处理,并调度其他的用户进程继续执行。

要实现进程阻塞,最重要的就是wait_queue。
通常使用

static DECLARE_WAIT_QUEUE_HEAD(my_wq);
DECLARE_WAIT_QUEUE_HEAD(my_wq);

来定义一个全局结构体wati_queue。

这个宏的等效代码如下:

static wait_queue_head_t my_wq;
init_waitqueue_head(&my_wq);

wait_event_interruptible(my_wq,condition)函数,将调用内核服务,将current_user_process的PCB关联到my_wq队列中,然后内核将进程从TASK_RUN队列中删除,并将进程放入内核维护的TASK_INTERRUPTIBLE中。这样,进程就被内核休眠了。
对应的,wake_up_interruptible(&my_wq)函数,将调用内核服务,遍历my_wq中关联的PCB,把他们唤醒。

当进程不在RUN队列中时,是不会被调度执行的。如果没有别的进程将它唤醒,它将始终休眠。所以,必须要有别的进程对应使用wake_up服务。
推荐的做法是,在fops中将用户进程休眠,而在中断ISR中,将用户进程唤醒。

另一种是wait_event_timeout函数,它提供了一个默认的唤醒机制,就是TIMER唤醒。从而避免进程出现永久休眠。

wait_event函数可以把进程添加到TASK_UNINTERRUPTIBLE队列中。
区别在于,uninterruptible是不能被signal唤醒的,所以它只能被wake_up函数唤醒。但是interruptible除了有可能是wake_up_interruptible函数唤醒,还有可能是被signal唤醒的,所以它在恢复执行时,还需要判断,是否是因为signal而被唤醒。

有了这些基础知识。我们来看看,支持阻塞的驱动模块,是如何使用这些内核服务的。
为了使设备支持阻塞,我们需要为它设置wait_queue_head。
在多进程程序设计中,event是用结构体对象来表达的。一个event对应一个对象实体。
例如,最简单的front and rear stage架构中,event可能只是一个公共的int数。
event对应的是一个对象实体,event occured 则对应的是event的成员的数据发生了改变。front stage 修改event的成员的数据为协议约定的值,当rear stage运行时,检查event的成员的值,根据值来判断event occured,处理完后,修改event的成员值为cleared 状态。
从这个角度看,event是front stage和rear stage的通信机制。

在linux中,event是kernel和user_process的一种通信机制。
event仍然是对象实体,event occured仍然是对象实体的数据改变。
对于进程调度所需要的event而言,kernel依据event的数据,作为调度的判据。
进程修改event,产生了event occured状态。当kernel在运行时,检查event的数据,并做出处理,处理完后,修改event的数据,从而实现event cleared状态。
在wait_queue_head这个例子下,
进程通过内核服务wait_event使进程修改wait_queue_head,即把PCB加入wait_queue_head中,产生了event occured状态。当另一个进程通过内核服务wake_up使得kernel运行时,kernel就去检查wait_queue_head的链表元素,并做出唤醒处理,处理完成,会从wait_queue_head中删除PCB,从而产生event cleared状态。

总结如下,
event是一个对象实体。
user_process通过修改event的数据,产生Event Request。
kernel通过检查event的数据,并做出对应的处理,产生Event Action。
kernel通过修改event的数据,产生Event Response。

来看一个具体的例子。

struct globalfifo_dev{
	struct cdev cdev;
	uint current_len;
	uchar mem[SIZE];
	wait_queue_head_t r_wq;
	wait_queue_head_t w_wq;
};

在这个DEV的结构体中,我们定义了两个wait_queue_head,r_wq和w_wq。

static int __init globalfifo_init(void)
{
	...
	init_waitqueue_head(&globalfifo_devp->r_wq);
	init_waitqueue_head(&globalfifo_devp->w_wq);
	...
}
module_init(globalfifo_init);

在模块加载函数中,我们初始化了两个wait_queue_head。作为两个event。

static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
	int ret;
	struct globalfifo_dev * devp = filp->private_data; 
	
	if(fifo_empty){
		wait_event_interruptible(devp->r_wq, !fifo_empty);
	}
	
	ret = fifo_to_user(buf, devp->mem, count);

	if(!fifo_full){
		wake_up_interruptible(&devp->w_wq);
	}
	return ret ;
}

static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
	int ret;
	struct globalfifo_dev * devp = filp->private_data; 

	if(fifo_full){
		wait_event_interruptible(devp->w_wq, !fifo_full);
	}
	
	ret = fifo_from_user(devp->mem, buf, count);
	wake_up_interruptible(&devp->r_wq);
	return ret ;
}

在这个例子里,驱动利用两个wait_queue_head支持了阻塞IO。
当用户进程A请求DRIVER提供read服务时,内核调用了read函数。
在read函数中,对资源进行了判断,如果资源为空,说明不可读,那么read函数会使用内核服务函数wait_event将当前用户进程休眠,即,进程A被内核休眠。进程A在特定的event,即r_wq上,发起了一个EventRequest。
当用户进程B请求DRIVER提供write服务时,内核调用了write函数。
在write函数中,使用内核服务函数wake_up,检查event的数据,即r_wq的关联链表,检查是否有进程阻塞在该event上。然后将关联的进程唤醒,实现EventAction。最后,把成功被唤醒的进程从event中删除,完成一个EventResponse。即,进程B中,将阻塞在r_wq上的进程A唤醒。

我们可以清晰的看出,进程A和kernel的基于event的通信,以及进程B和kernel基于event的通信。进程A和进程B并不直接通信,但是通过kernel的统一管理,间接实现了通信。进程A通知内核,自己有一个EventRequest,进程B通知内核,自己有一个EventResponse。内核则负责EventAction。

另一个方向上,
当用户进程B请求DRIVER提供write服务时,内核调用了write函数。
在write函数中,对资源进行了判断,如果资源为满,说明不可写,那么write函数会使用内核服务函数wait_event将当前用户进程休眠,即进程B被内核休眠。进程B,在特定的event,即w_wq上,发起了一个EventRequest。
当用户进程A请求DRIVER提供read服务时,内核调用了read函数。
在read函数中,使用内核服务函数wake_up,检查event的数据,即w_wq的关联链表,检查是否有进程阻塞在该event上。然后将关联的进程唤醒,实现EventAction。最后,把成功被唤醒的进程从event中删除,完成一个EventResponse。即,进程A中,将阻塞在w_wq上的进程B唤醒。

这是基本的producer and customer模型。
customer作为需要资源的一方,通知内核将自己休眠,而producer作为提供资源的一方,通知内核唤醒customer。

linux提供了另一种IO机制,就是POLL机制。
当用户进程中请求了内核服务poll时,内核会轮询FD_SET中的fd。只要有fd可读或者可写的,就是return。如果遍历完毕,都没有任何一个fd可以读写,才会将当前用户进程阻塞。另一方面,如果任何一个fd变得可读写,进程就会被唤醒。
如果驱动支持POLL机制,就需要在驱动模块中实现poll函数。
当内核服务poll执行时,内核依次调用FD_SET中的DEV多对应的FOPS中的poll函数。传入一个FILE的句柄和一个POLL_TABLE的句柄。
poll函数的主要工作是,
1)将进程关联到轮询表中。
2)向内核返回设备的POLLSTATUS。
来看一个具体的例子。

static unsigned int vser_poll(struct file *filp, struct poll_table_struct *polltbl)
{
	unsigned int mask = 0;
	struct ver_dev *devp = filp->private_data;

	...
	poll_wait(filp, &devp->r_wq, polltbl);
	poll_wait(filp, &devp->w_wq,polltbl);
	...

	if(!fifo_empty){
		mask |= POLLIN | POLLNORM;
	}
	if(!fifo_full){
		mask |= POLLOUT | POLLNORM;
	}
	return mask;
}

其中,首先是将wait_queue_head注册到POLL_TABLE中。然后是将POLLSTATUS返回给内核。
poll_wait内核服务并不像wait_event内核服务一样,会请求内核阻塞当前用户进程,它只是将FILE,WAIT_QUEUE,POLLTABLE三者建立关联,将进程的PCB加入WAIT_QUEUE中。
内核在遍历了所有的驱动的poll函数后,如果都没有POLLIN或者POLLOUT,这个时候内核才会休眠当前用户进程。
我们知道,有休眠,就一定要有唤醒。
通常是在ISR中针对特定的event唤醒关联到该event的进程,使用的是poll_wake内核服务函数。
由于每个驱动的poll函数,都将当前用户进程关联到了自己的相关event的链表中,所以,每个驱动的每个event都有可能将进程唤醒。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值