提出问题:若驱动程序无法立即满足请求,该如何响应? 比如:当数据不可用时调用read,或是在缓冲区已满时,调用write
解决问题:驱动程序应该(默认)该阻塞进程,将其置入休眠状态直到请求可继续。
休眠:
当一个进程被置入休眠时,它会被标记为一种特殊状态并从调度器运行队列中移走,直到某些情况下修改了这个状态,才能运行该进程。
安全进入休眠两原则:
1.永远不要在原子上下文中进入休眠。(原子上下文:在执行多个步骤时,不能有任何的并发访问。这意味着,驱动程序不能再拥有自旋锁,seqlock,或是RCU锁时,休眠)
2.对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真
临界区 vs 原子上下文
原子上下本:一般说来,具体指在中断,软中断,或是拥有自旋锁的时候。
临界区:每次只允许一个进程进入临界区,进入后不允许其它进程访问。
other question:
要休眠进程,必须有一个前提:有人能唤醒进程,而起这个人必须知道在哪儿能唤醒进程,这里,就引入了“等待队列”这个概念。
等待队列:就是一个进程链表(我的理解:是一个休眠进程链表),其中包含了等待某个特定事件的所有进程。
等待队列头:wait_queue_head_t,定义在<linux/wait.h>
定义方法:静态 DECLARE_QUEUE_HEAD(name)
动态 wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
简单休眠
linux最简单的休眠方式是wait_event(queue,condition)及其变种,在实现休眠的同时,它也检查进程等待的条件。四种wait_event形式如下:
wait_event(queue,condition);/*不可中断休眠,不推荐*/
wait_event_interruptible(queue,condition);/*推荐,返回非零值意味着休眠被中断,且驱动应返回-ERESTARTSYS*/
wait_event_timeout(queue,condition,timeout);
wait_event_interruptible_timeout(queue,conditon,timeout);/*有限的时间的休眠,若超时,则不管条件为何值返回0*/
唤醒休眠进程的函数:wake_up
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head *queue);
惯例:用wake_up唤醒wait_event,用wake_up_interruptible唤醒wait_event_interruptible
休眠与唤醒 实例分析:
本例实现效果为:任何从该设备上读取的进程均被置于休眠。只要某个进程向给设备写入,所有休眠的进程就会被唤醒。
static DECLARE_WAIT_QUEUE_HEAD(wq); static int flag =0; ssize_t sleepy_read(struct file *filp,char __user *buf,size_t count,loff_t *pos) { pirntk(KERN_DEBUG "process %i (%s) going to sleep\n",current->pid,current->comm); wait_event_interruptible(wq,flag!=0); flag=0; printk(KERN_DEBUG "awoken %i (%s) \n",current->pid,current->comm); return 0; } ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos) { printk(KERN_DEBUG "process %i (%s) awakening the readers ...\n",current->pid,current->comm); flag=1; wake_up_interruptible(&wq); return count; /*成功并避免重试*/ }
阻塞与非阻塞类操作
小知识点:
操作系统中睡眠、阻塞、挂起的区别形象解释
操作系统中睡眠、阻塞、挂起的区别形象解释 挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。 使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。 线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能怪雇工,肯定你这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。
首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
全功能的 read 和 write 方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再<linux/fcntl.h> ,被<linux/fs.h>自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。O_NONBLOCK 标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠.
其实不一定只有read 和 write 方法有阻塞操作,open也可以有阻塞操作。
1.如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单的返回-EAGAIN。
2.非阻塞型操作会立即返回,使得应用程序可以查询数据。在处理非阻塞型文件时,应用程序调用stdio函数必须非常小心,因为很容易就把一个非阻塞返回误认为EOF,所以必须始终检查errno。
3.有些驱动程序还为O_NONBLOCK实现了特殊的语义。例如,在磁带还没有插入时打开一个磁带设备通常会阻塞,如果磁带驱动程序使用O_NONBLOCK打开的,则不管磁带在不在,open都会立即成功返回。
4.只有read,write,open文件操作受非阻塞标志的影响。
read负责管理阻塞型和非阻塞型输入,如下所示:
static ssize_t scull_p_read (struct file *filp,char __user *buf,size_t count,loff_t *f_pos) { struct scull_pipe *dev = filp->private_data; if(down_interruptible(&dev)) return -ERRESTARTSYS; while(dev->rp == dev->wp){/*无数据可读*/ up(&dev->sem); /*释放锁*/ 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; } /*数据已就绪,返回*/ if(dev->wp > dev->rp) count=min(count,(size_t) (dev->wp - dev->rp)); else /*写入指针回卷,返回数据直到dev->end*/ count = min(count,(size_t)(dev->end - dev->rp)) if(copy_to_user(buf,dev->rp,count)){ up(&dev->sem); return -EFAULT; } dev->rp += count; if(dev->rp == dev->end) dev->rp = dev->buffer;/*回卷*/ up(&dev->sem); /*最后,唤醒所有写入者并返回*/ wake_up_interruptible(&dev->outq); PDEBUG("\%s\" did read %li bytes \n ",current->comm,(long)count"); return count; }
代码分析:
while循环在拥有设备信号量时测试缓冲区。如果其中有数据,则可以立即将数据返回给用户而不需要休眠,这样,整个循环体就被跳过了。相反,如果缓冲区为空,则必须休眠。但在休眠之前必须释放设备信号量,因为如果在拥有该信号量时休眠,任何写入者都没有机会来唤醒。在释放信号量之后,快速检测用户请求的是否是非阻塞I/O,如果是,则返回,否则调用wait_event_interruptible。
高级休眠:
进程休眠步骤:
1.分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列
2.设置进程的状态,将其标记为休眠在 <linux/sched.h> 中定义有几个任务状态: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.建立并初始化一个等待队列入口。
方法1: DEFINE_WAIT(my_wait); 方法2: wait_queue_t my_wait; init_wait(&my_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.在调用prepaer_to_wait之后,进程即可调用schedule,当然在这之前,应确保仍有必有等待。一旦schedule返回,就到了清理时间了。
void finesh_wait(wait_queue_head_t *queue,wait_queue_t *wait);
独占等待
当一个进程调用 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_nr(wait_queue_head_t *queue, int nr);
wake_up_all(wait_queue_head_t*queue);
wake_up_interruptible_sync(wait_queue_head_t *queue);
wake_up_interruptible(wait_queue_head_t *queue);
/*wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。wake_up_interruptible 同样, 除了它跳过处于不可中断休眠的进程。它们在返回之前, 使一个或多个进程被唤醒、被调度(如果它们被从一个原子上下文调用, 这就不会发生).*/
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);
/*这些函数类似 wake_up, 除了它们能够唤醒多达 nr 个独占等待者, 而不只是一个. 注意传递 0 被解释为请求所有的互斥等待者都被唤醒*/
wake_up_interruptible_all(wait_queue_head_t *queue);
/*这种 wake_up 唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)*/
/*一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果你需要不要被调度出处理器时,可以使用 wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/
poll and select
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读写阻塞的状态下,并影响了其它文件的读写。为了避免这种情况的发生,则必须使用多输入输出流又不想阻塞在他们任何一个上的应用程序常将非阻塞的I/O和poll(system V),select(BSD Unix),epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标识读写相应的文件,实现非阻塞的读写。这些系统调用功能相同:允许进程来决定它是否可读或写一个或多个文件而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中的poll方法的支持。poll返回不同的标识,告诉主进程文件是否可以读写,其原型:
<linux/poll.h> unsigned int (*poll) (struct file *filp,poll_table *wait);
实现这个设备方法分两步:
1.在一个或多个可指示查询状态变化的等待队列上调用poll_wait,如果没有文件描述符可用来执行I/O,内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符,驱动通过调用函数poll_wait增加一个队列到poll_wait结构,原型:
void poll_wait(struct file *,wait_queue_head_t *,poll_table*);
2.返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过<linux/poll.h>定义)用来指示可能的操作:
标志 含义 POLLIN 如果设备无阻塞的读,就返回该值 POLLRDNORM 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA) POLLRDBAND 如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中 POLLPRI 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理 POLLHUP 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。 POLLERR 如果设备发生错误,就返回该值。 POLLOUT 如果设备可以无阻塞地些,就返回该值 POLLWRNORM 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM) POLLWRBAND 于POLLRDBAND类似
使用举例:
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);
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM; /* readable */
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM; /* writable */
up(&dev->sem);
return mask;
}
与read与write的交互
正确实现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 方法。
永远不要让write调用在返回前等待数据传输的结束,即使O_NONBLOCK标志被清除。这是因为很多应用程序使用select来检查write是否会阻塞。如果报告设备可以写入,调用就不能被阻塞。如果使用设备的程序需要保证输出缓冲区中的数据确实已经被传送出去,驱动程序就必须提供一个fsync方法。
刷新待处理输出
若一些应用程序需要确保数据被发送到设备,就必须实现fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被设置,即便这需要一些时间。其原型是:
int(*fsync)(structfile*file,struct dentry*dentry,int datasync);
底层的数据结构
异步通知
通过使用异步通知,应用程序可以在数据可用时收到一个信号,而无需不停地轮询。
启用步骤:
(1)它们指定一个进程作为文件的拥有者:使用 fcntl 系统调用发出 F_SETOWN 命令,这个拥有者进程的 ID 被保存在 filp->f_owner。目的:让内核知道信号到达时该通知哪个进程。
(2)使用 fcntl 系统调用,通过 F_SETFL 命令设置 FASYNC 标志。
内核操作过程
1.F_SETOWN被调用时filp->f_owner被赋值。
2. 当 F_SETFL 被执行来打开 FASYNC, 驱动的 fasync 方法被调用.这个标志在文件被打开时缺省地被清除。
3. 当数据到达时,所有的注册异步通知的进程都会被发送一个 SIGIO 信号。
Linux 提供的通用方法是基于一个数据结构和两个函数,定义在<linux/fs.h>。
数据结构:
struct fasync_struct{
int magic;
int fa_fd;
struct fasync_struct *fa_next;/* singly linked list */
struct file *fa_file;
};
驱动调用的两个函数的原型:
int fasync_helper(int fd,structfile*filp,int mode, struct fasync_struct**fa);
void kill_fasync(struct fasync_struct**fa,int sig, int band);
当一个打开的文件的FASYNC标志被修改时,调用fasync_helper 来从相关的进程列表中添加或去除文件。除了最后一个参数, 其他所有参数都时被提供给 fasync 方法的相同参数并被直接传递。 当数据到达时,kill_fasync 被用来通知相关的进程,它的参数是被传递的信号(常常是 SIGIO)和 band(几乎都是 POLL_IN)。
这是 scullpipe 实现 fasync 方法的:
staticint scull_p_fasync(int fd,struct file*filp,int mode)
{
struct scull_pipe *dev = filp->private_data;
return fasync_helper(fd, filp, mode,&dev->async_queue);
}
当数据到达, 下面的语句必须被执行来通知异步读者. 因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生, 这个语句出现在 scullpipe 的 write 方法中:
if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 注意, 一些设备也针对设备可写而实现了异步通知,在这个情况,kill_fasnyc 必须以 POLL_OUT 模式调用.*/
当文件被关闭时必须调用fasync 方法,来从活动的异步读取进程列表中删除该文件。尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。 以下是 scullpipe 的 release 方法的一部分:
/* remove this filp from the asynchronously notified filp's */ scull_p_fasync(-1, filp, 0);
异步通知使用的数据结构和 struct wait_queue 几乎相同,因为他们都涉及等待事件。区别异步通知用 struct file 替代 struct task_struct. 队列中的 file 用获取 f_owner, 一边给进程发送信号。