现在的位置: 首页 > 综合 > 正文

Linux设备驱动程序学习(5)-高级字符驱动程序操作〔(2)阻塞型I/O和休眠〕

2012年09月23日 ⁄ 综合 ⁄ 共 8576字 ⁄ 字号 评论关闭

Linux设备驱动程序学习(5)
-高级字符驱动程序操作〔(2)阻塞型I/O和休眠〕

这一部分主要讨论:如果驱动程序无法立即满足请求,该如何响应?(65865346)
一、休眠
进程被置为休眠,意味着它被标识为处于一个特殊的状态并且从调度器的运行队列中移走。这个进程将不被在任何 CPU 上调度,即将不会运行。 直到发生某些事情改变了那个状态。安全地进入休眠的两条规则:
(1) 永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或者 RCU 锁时不能睡眠;关闭中断也不能睡眠。持有一个信号量时休眠是合法的,但你应当仔细查看代码:如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。因此发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒你的进程。
(2)当进程被唤醒,它并不知道休眠了多长时间以及休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在它之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应
除非确信其他进程会在其他地方唤醒休眠的进程,否则也不能睡眠。使进程可被找到意味着:需要维护一个称为等待队列的数据结构。它是一个进程链表,其中饱含了等待某个特定事件的所有进程。在 Linux 中, 一个等待队列由一个wait_queue_head_t 结构体来管理,其定义在中。wait_queue_head_t
类型的数据结构非常简单: 

struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
它包含一个自旋锁和一个链表。这个链表是一个等待队列入口,它被声明做 wait_queue_t。wait_queue_head_t包含关于睡眠进程的信息和它想怎样被唤醒。 
简单休眠(其实是高级休眠的宏)
Linux 内核中最简单的休眠方式是称为 wait_event的宏(及其变种),它实现了休眠和进程等待的条件的检查。形式如下:
wait_event(queue, condition)/*不可中断休眠,不推荐*/
wait_event_interruptible(queue, condition)/*推荐,返回非零值意味着休眠被中断,且驱动应返回 -ERESTARTSYS*/
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
/*有限的时间的休眠;若超时,则不管条件为何值返回0,*/
唤醒休眠进程的函数称为 wake_up,形式如下:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t
*queue);

惯例:用 wake_up 唤醒 wait_event ;用 wake_up_interruptible
唤醒wait_event_interruptible。

简单休眠实验
模块程序链接:
sleepy
模块测试程序链接
sleepy-test
实验现象:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod sleepy.ko
[Tekkaman2440@SBC2440V4]#cd /dev/
[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
10 misc
13 input
14 sound
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 sleepy
253 usb_endpoint
254 rtc
Block devices:
  1 ramdisk
256 rfd
  7 loop
31 mtdblock
93 nftl
96 inftl
179 mmc
[Tekkaman2440@SBC2440V4]#mknod -m 666 sleepy c 252 0
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./sleepy_testr&
[Tekkaman2440@SBC2440V4]#./sleepy_testr&
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  799 root 1336 S ./sleepy_testr  800 root 1336 S ./sleepy_testr
  802 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./sleepy_testw
read code=0
write code=0
[2] + Done ./sleepy_testr
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  799 root 1336 S ./sleepy_testr
  804 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./sleepy_testw
write code=0
[Tekkaman2440@SBC2440V4]#read code=0
[1] + Done ./sleepy_testr
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW [watchdog/0]
    5 root SW [events/0]
    6 root SW [khelper]
   59 root SW [kblockd/0]
   60 root SW [ksuspend_usbd]
   63 root SW [khubd]
   65 root SW [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW [kswapd0]
   80 root SW [aio/0]
  707 root SW [mtdblockd]
  708 root SW [nftld]
  709 root SW [inftld]
  710 root SW [rfdd]
  742 root SW [kpsmoused]
  751 root SW [kmmcd]
  769 root SW [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  783 root 1744 S init
  787 root 1744 S init
  806 root 1744 R ps
阻塞和非阻塞操作
全功能的 read 和 write 方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再 ,被 自动包含)。浏览源码,会发现O_NONBLOCK
的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。O_NONBLOCK 标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠. 

其实不一定只有read 和 write 方法有阻塞操作,open也可以有阻塞操作。后面会见到。而我的项目有一个和CPLD的接口的驱动,我决定要在ioctl 中使用阻塞。
以下是后面的scullpipe实验的有关阻塞的代码,我觉得写得很好,先结合书上的介绍看看吧:
while (dev->rp == dev->wp)      { /* nothing to read */
     up(&dev->sem); /* release the lock */
     if (filp->f_flags & O_NONBLOCK)
         return -EAGAIN;
     PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
     if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
        return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ /* otherwise loop, but first reacquire the lock */
     if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
}        /* ok, data is there, return something */
......
高级休眠
步骤:
(1)分配和初始化一个 wait_queue_t 结构, 随后将其添加到正确的等待队列。
(2)设置进程状态,标记为休眠。在  中定义有几个任务状态:TASK_RUNNING 意思是进程能够运行。有 2 个状态指示一个进程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。2.6 内核的驱动代码通常不需要直接操作进程状态。但如果需要这样做使用的代码是: 
void set_current_state(int new_state); 
在老的代码中, 你常常见到如此的东西:current->state = TASK_INTERRUPTIBLE; 但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。
(3) 最后一步是放弃处理器。 但必须先检查进入休眠的条件。如果不做检查会引入竞态:
如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下: 

if (!condition)
    schedule();
如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 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); 
认真地看简单休眠中的 wait_event(queue, condition) 和 wait_event_interruptible(queue, condition) 底层源码会发现,其实他们只是手工休眠中的函数的组合。所以怕麻烦的话还是用wait_event比较好。
独占等待
当一个进程调用 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
的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/ 

poll 和 select
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读写阻塞的状态,并影响了其他文件的读写。为了避免这种情况,在必须使用多输入输出流又不想阻塞在它们任何一个上的应用程序常将非阻塞 I/O 和 poll(System V)、select(BSD Unix)、 epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标志读写相应的文件,实现非阻塞的读写。这些系统调用功能相同:
允许进程来决定它是否可读或写一个或多个文件而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中poll 方法的支持,poll返回不同的标志,告诉主进程文件是否可以读写,其原型(定义在
 ):
unsigned int (*poll) (struct file *filp, poll_table *wait);
实现这个设备方法分两步:
1. 在一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 如果没有文件描述符可用来执行 I/O, 内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符. 驱动通过调用函数 poll_wait增加一个等待队列到 poll_table 结构,原型:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *); 
2. 返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过  定义)用来指示可能的操作:
标志
含义
POLLIN
如果设备无阻塞的读,就返回该值
POLLRDNORM
通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA)
POLLRDBAND
如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中
POLLPRI
如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理
POLLHUP
当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。
POLLERR
如果设备发生错误,就返回该值。
POLLOUT
如果设备可以无阻塞地些,就返回该值
POLLWRNORM
设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM)
POLLWRBAND
于POLLRDBAND类似
考虑 poll 方法的 scullpipe 实现:
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
        struct scull_pipe *dev = filp->private_data;
        unsigned int mask = 0;
        /*
        * The buffer is circular; it is considered full
        * if "wp" is right behind "rp" and empty if the
        * two are equal. 
        */
        down(&dev->sem);

抱歉!评论已关闭.