linux设备驱动学习(九)–高级字符设备驱动

高级休眠

执行步骤:1.将进程置于休眠的第一个步骤是分配并初始化一个wait_queue_t结构,然后加入到对应的等待序列。

2.设置进城的状态,将其标记问休眠。

TASK_RUNNING表示进程可运行,尽管进程并不一定在任何给定时间都运行在某个处理器上。

TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE对应两种休眠状态。

在2.6内核中,通常不需要驱动程序代码来操作进城状态,但是如果需要:

void set_current_state(int new_state);

在老代码中:

current->state = TASK_INTERRUPTIBLE;

但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。

3.放弃处理器。在这之前必须检查休眠等待的时间。考虑如下情况:在我们的进程被唤醒之前,condition已经满足,这时就存在竟态。我们可能会失去被唤醒的机会,从而休眠很长的时间。解决方法如下:

if(!condition)

schedule();

如果在忙于之前的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。如果条件成立则直接跳过schedule调用,进入执行状态。

如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠(condition为1)而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING,还必要从等待队列中去除这个进程,否则它可能被多次唤醒。

手工休眠:

(1)创建和初始化一个等待队列。常由宏定义完成DEFINE_WAIT(my_wait);name 是等待队列入口项的名字. 也可以用2步来做:wait_queue_t my_wait;init_wait(&my_wait);常用的做法是放一个 DEFINE_WAIT 在循环的顶部,来实现休眠。

(2)添加等待队列入口到队列,并设置进程状态:void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); /*queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)。

在设置晚进程状态之后,一般都要对条件进行检查,通过后边的代码分析。

(3)在检查确认仍然需要休眠之后调用 schedule

schedule(); //如果仍需要休眠,则把进程调度走

(4)schedule 返回,就到了清理时间:void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);

static int scull_getwritespace(struct scull_pipe *dev,struct file *filp)

{

while(spacefree(dev) == 0){ //缓冲区已满

DEFINE_WAIT(wait); 设置等待队列入口。

up(&dev->sem);

if(filp->f_flags & O_NONBLOCK)

return -EAGAIN;

PDEBUG("/"%s/" writing:going to sleep/n",current->comm);

prepare_to_wait(&dev->outq,&wait,TASK_INTERRUPTIBLE);//进入真正的休眠

if(spacefree(dev) == 0) // 这里必须要进行检查:考虑如下情况,在我们进入while循环之后,但在将自己放到等待队列之前,缓冲区变的可用。若不做这个检查,则会失去被唤醒的机会。

schedule();

finish_wait(&dev->outq,&wait);

if(signal_pending(current)) //对signal_pending的调用告诉我们,是否因为信号而被唤醒。如果是,我们需要返回给用户并让用户再次重拾;否则,我们将获取信号量,检测空间。

return -ERESTRATSYS;

if(down_interruptible(&dev->sem)

return -ERESTRATSYS;

}

return 0;

}

独占等待

当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2 个重要的不同:

(1)当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。

(2)当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒所有的非独占等待。

采用独占等待要满足 2 个条件:

(1)希望对资源进行有效竞争;

(2)当资源可用时,唤醒一个进程就足够来完全消耗资源。

使一个进程进入独占等待,可调用:

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

注意:无法使用 wait_event 和它的变体来进行独占等待.

唤醒的相关函数

很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:

wake_up(wait_queue_head_t *queue); wake_up_interruptible(wait_queue_head_t *queue); /*wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。wake_up_interruptible 同样, 除了它跳过处于不可中断休眠的进程。它们在返回之前, 使一个或多个进程被唤醒、被调度(如果它们被从一个原子上下文调用, 这就不会发生).*/


wake_up_nr(wait_queue_head_t *queue, int nr); wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); /*这些函数类似 wake_up, 除了它们能够唤醒多达 nr 个独占等待者, 而不只是一个. 注意传递 0 被解释为请求所有的互斥等待者都被唤醒*/


wake_up_all(wait_queue_head_t *queue); wake_up_interruptible_all(wait_queue_head_t *queue); /*这种 wake_up 唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)*/


wake_up_interruptible_sync(wait_queue_head_t *queue); /*一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果你需要不要被调度出处理器时,可以使用 wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/

转自:http://blog.chinaunix.net/u1/34474/showart.php?id=413183

poll和select

poll、 select、epoll:都允许进程决定是否可以对一个或多个打开的文件作非阻塞的读取或写入。这些调用也会阻塞进程,知道给定的文件描述符集合中任何一个可读取或写入。

对上述系统调用的支持需要驱动中的poll函数:

unsigned int (*poll)(struct file *filp,poll_table *wait)

当用户程序在驱动程序关联的文件描述符上执行poll、select、epoll系统调用时,该驱动程序将被调用。分为两步:

1.在一个或多个可指示poll状态变化的等待队列上调用poll_wait。如果当前没有可用来执行的I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。通过调用poll_wait函数,向poll_table结构添加一个等待队列。

void poll_wait(struct file *,struct wait_queue_head_t *,poll_table *);

2.返回一个位掩码:描述可能不必阻塞就立刻进行的操作

标志

含义

POLLIN

如果设备无阻塞的读,就返回该值

POLLRDNORM

通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORM)

POLLRDBAND

如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中

POLLPRI

如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理

POLLHUP

当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。

POLLERR

如果设备发生错误,就返回该值。

POLLOUT

如果设备可以无阻塞地写,就返回该值

POLLWRNORM

设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLWRNORM)

POLLWRBAND

于POLLRDBAND类似

scullpipe的poll实现:

static unsigned int scull_p_poll(struct file *filp,poll_table *wait)

{

unsigned int mask = 0;

struct scull_pipe *dev = filp->private_data;

down(&dev->sem);

poll_wait(filp,&dev->inq,wait);

poll_wait(filp,&dev->outq,wait); //驱动程序只需要调用就可以,不用关注其细节

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

mask |= POLLIN|POLLRDNORM; //可读取

if(spacefree(dev))

make |= POLLOUT|POLLWRNORM; //可写入

up(&dev->sem);

return mask;

}

与read和write进行交互

poll和select调用的目的是确定接下来的I/O操作是否会阻塞。poll和select的更重要用途他们可以是应用程序同时等待多个数据流。

正确实现poll调用的规则:

从设备读取数据:

1.如果在输入缓冲中有数据,read 调用应当立刻返回,即便数据少于应用程序要求的,并确保其他的数据会很快到达。 如果方便,可一直返回小于请求的数据,但至少返回一个字节。在这个情况下,poll 应当返回 POLLIN|POLLRDNORM。

2.如果在输入缓冲中无数据,read默认必须阻塞直到有一个字节。若O_NONBLOCK 被置位,read 立刻返回 -EAGIN 。在这个情况下,poll 必须报告这个设备是不可读(清零POLLIN|POLLRDNORM)的直到至少一个字节到达。

3.若处于文件尾,不管是否阻塞,read 应当立刻返回0,且poll 应该返回POLLHUP

向设备写数据:

1.若输出缓冲有空间,write 应立即返回。它可接受小于调用所请求的数据,但至少必须接受一个字节。在这个情况下,poll应返回 POLLOUT|POLLWRNORM。

2.若输出缓冲是满的,write默认阻塞直到一些空间被释放。若 O_NOBLOCK 被设置,write 立刻返回一个 -EAGAIN。在这些情况下, poll 应当报告文件是不可写的(清零POLLOUT|POLLWRNORM). 若设备不能接受任何多余数据, 不管是否设置了 O_NONBLOCK,write 应返回 -ENOSPC("设备上没有空间")。

3.永远不要让write在返回前等待数据的传输结束,即使O_NONBLOCK 被清除。若程序想保证它加入到输出缓冲中的数据被真正传送, 驱动必须提供一个 fsync 方法。

刷新待处理输出若一些应用程序需要确保数据被发送到设备,就实现必须fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被设置,即便这需要一些时间。其原型是:

int (*fsync) (struct file *file, struct dentry *dentry, int datasync);

例如快设备总是用通用的block_fsync来实现这个方法,block_fasync会一次刷新新设备的缓冲块。

底层的数据结构:

当用户程序调用poll,select,epoll时,内核会调用有该系统调用引用的全部文件的poll方法,并向他们传递同一个poll_table,注意是同一个。对于select和poll系统调用,poll_table是一个包含poll_table_entry结构的内存页链表(poll_table中包含了多个poll_table_entry结构,每个结构对应于一个文件的poll方法)。

struct poll_table_entry {struct file * filp;wait_queue_t wait;wait_queue_head_t * wait_address;};

在对上述poll_table进行轮询时(也即对poll_table_entry进行轮询时),如果没有一个驱动程序指明可以进行非阻塞I/O,这个poll就进入休眠,知道休眠在其上的一个或多个等待队列被唤醒。

对 poll_wait 的调用有时还会将进程添加到给定的等待队列。整个的结构必须由内核维护,在 poll 或者 select 返回前,进程可从所有的队列中去除。

当 poll 调用完成,poll_table 结构被重新分配, 所有的之前加入到 poll 表的等待队列入口都会从表和它们的等待队列中移出.

poll背后的数据结构

觉得自己做的到和不做的到,其实只在一念之间

linux设备驱动学习(九)–高级字符设备驱动

相关文章:

你感兴趣的文章:

标签云: