📄 (ldd) ch06-时间流(转载).txt
字号:
统可以调度其他任务;当前任务除了释放CPU之外不做任何工作,但是它仍在任务队列中
。如果它是系统中唯一的可运行的进程,它还会被运行(系统调用调度器,调度器还是同
一个进程,此进程又再调用调度器,然后...)。换句话说,机器的负载(系统中运行的进
程个数)至少为1,而idle空闲进程(进程为0,历史性的被称为"swapper")绝不会被运行
。尽管这个问题看来无所谓,当系统空闲时运行idle空闲进程可以减轻处理器负载,降
低处理器温度,延长处理器寿命,如果是手提电脑,电池的寿命也可延长。而且,延迟
期间实际上进程是在执行的,因此这段延迟还是记在它的运行时间上的。运行命令
time cat /proc/jitsched就可以发现到这一点。
time cat /proc/jitsched就可以发现到这一点。
尽管有些毛病,这种循环延迟还是提供了一种有点“脏” 但比较快的监控驱动
程序工作的途径。如果模块中的臭虫(bug)会锁死整个系统,在每个用于调试的printk语
句后都添加一小段延迟,可以保证在处理器碰到令人厌恶的臭虫被锁死之前,所有的打
印消息都能进入系统日志(system log)中。如果没有这样的延迟,这些消息能进入内存
缓冲区,但在klogd得到运行前系统可能已经被锁住了。
还有其他更好的获得延迟的方法。在内核态下让进程进入睡眠态的正确方式是设
置current->timeout后睡眠在一个等待队列上。调度器每次运行时都会比较进程的timeo
ut值和当前的jiffies值,如果timeout值小于等于当前时间,那么不管它的等待队列如
何进程都会被唤醒。只要没有系统事件唤醒进程使它离开等待队列,那么一旦当前时间
达到timeout值,调度器就唤醒睡眠进程。
这种延迟实现如下:
struct wait_queue *wait=NULL;
struct wait_queue *wait=NULL;
current->timeout=j;
interruptible_sleep_on(&wait);
注意要调用interruptible_sleep_on而不是sleep_on,因为调度器不检查不可中
断的进程的timeout值-这种进程的睡眠即使超时也不被中断。因此,如果你调用sleep_
on,就无法中断该睡眠进程。你可以通过读/proc/jitqueue文件来测试上面的代码。
Timeout域是个很有意思的系统资源。它可以用来实现阻塞的系统调用和计算延
迟。如果硬件保证只要不出错就能在确定的时间内给出响应,那么驱动程序就可以在恰
当设置timeout值后进入睡眠。例如,如果你有一个对海量存储的数据传输请求(读或者
写),而磁盘响应该请求比如说要1秒。如果你设置了timeout值,并且当前时间达到它了
,进程于是被唤醒,驱动程序开始处理这个请求。如果你使用这种技术,在进程被正常
唤醒之后timeout值应被清为零。而如果进程是因为timeout超时而被唤醒的,调度器会
清这个域,驱动程序就不必再做了。
清这个域,驱动程序就不必再做了。
你可能注意到了,如果目的只是插入延迟,这里并没有必要使用等待队列。实际
上,如下所示,用current->timeout而不用等待队列就可以达到目的:
current->timeout=j;
current->state=TASK_INTERRUPTIBLE;
schedule();
current->timeout=0;/* 重置timeout值*/
这段语句是在调用调度器之前先改变进程的状态。进程的状态被标记为TASK_INT
ERRUPTIBLE(与TASk_RUNNING相对应),这保证了该进程在超时前不会被再次运行(但其他
系统事件如信号可能会唤醒它)。这种延迟方法在文件/proc/jitself中实现了-这个名
字强调了,读进程是“自己进入睡眠的”,而不是通过调用sleep_on。
字强调了,读进程是“自己进入睡眠的”,而不是通过调用sleep_on。
短延迟
有时驱动程序需要非常短的延迟来和硬件同步。此时,使用jiffies值就不能达到目的。
这时就要用内核函数udelay*。它的原型如下:
#include <linux/delay.h>
void udelay(unsigned long usecs);
该函数在绝大多数体系结构上是作为内联函数编译的,并且使用软件循环将执行延迟指
定数量的微秒数。这里要用到BogoMips值:udelay利用了整数值loops_per_second,这
个值是在启动时计算BogoMips时得到的。
udelay函数只能用于获取较短的时间延迟,因为loops_per_second值的精度就只有8位,
所以当计算更长的延迟时会积累下相当大的误差。尽管运行的最大延迟将近1秒(因为更
长的延迟就要溢出),推荐的udelay函数的参数的最大值是取1000微秒(1毫秒)。
要特别注意的是udelay是个忙等待函数,在延迟的时间段内无法运行其他的任务。源码
见头文件<asm/delay.h>。
目前内核不支持大于1微秒而小于1个时钟滴答的延迟,但这不是个问题,因为延迟是给
硬件或者人去识别的。百分之一秒的时间间隔对人来说延迟精度足够了,而1毫秒对硬件
来说延迟时间也足够长。如果你真的需要其间的延迟间隔,你只要建立一个连续执行ude
lay(1000)函数的循环。
任务队列
许多驱动程序需要将任务延迟到以后处理,但又不想占用中断。Linux为此提供了两种方
法:任务队列和内核定时器。任务队列的使用很灵活,可以或长或短地延迟任务到以后
处理,在写中断处理程序时任务队列非常有用,在第9章“中断处理”中,我们还将在“
下半部处理”一节中继续讨论。内核定时器则用来调度任务在未来某个相对精确的时间
下半部处理”一节中继续讨论。内核定时器则用来调度任务在未来某个相对精确的时间
执行,将在本章的“内核定时器”一节中讨论。
要使用到任务队列的一个典型情形是,硬件不产生中断,但仍希望提供阻塞的读。此时
需要对设备进行轮询,但要小心地不使CPU负担过多无谓的操作。将读进程到指定的时间
后(例如,使用current->timeout变量)唤醒并不是个很好的方法,因为每次轮询需要两
次上下文切换,而且通常轮询机制在进程上下文之外才可能较好地实现。
类似的情形还有象不时地给简单的硬件设备提供输入。例如,有一个直接连接到并口的
步进马达,要求该马达能一步步地移动。在这种情况下,由控制进程通知设备驱动程序
进行移动,但实际上移动是在write返回后才一步步地进行的。
快速完成这类不固定的任务的恰当方法是注册任务在未来执行。内核提供了对任务“队
列”的支持,任务可以累积到队列上一块“运行”。你可以声明你自己的任务队列并且
随意地操纵它,或者也可以将你的任务注册到预定义的任务队列中去,由内核来运行它
。
下面一节将先概述任务队列,然后介绍预定义的任务队列,这让你可以开始进行一些有
趣的测试(如果出错也可能挂起系统),最后介绍如何运行你自己的任务队列。
任务队列的特性
任务队列是任务的一张列表,每个任务用一个函数指针和一个参数表示。任务运行时,
它接受一个void *类型的参数,返回值类型为void。而参数指针data可用来将一个数据
结构传入函数,或者可以被忽略。队列本身是结构(任务)的列表,为声明和操纵它们的
内核模块所拥有。这些模块全权负责这些数据结构的分配和释放;为此一般使用静态的
数据结构。
队列元素由下面这个结构来描述,这段代码是直接从头文件<linux/tqueue.h>拷贝下来
的:
struct tq_struct {
struct tq_struct *next; /* 激活的bh的链接表 */
struct tq_struct *next; /* 激活的bh的链接表 */
unsigned long sync; /* 必须初始化为零 */
void (*routine)(void *); /* 调用的函数 */
void *data; /* 传递给函数的参数 */
};
第一行注释中的bh指的是下半部处理程序(bottom-half)。下半部处理程序是“
中断处理处理程序的下半部”;我们将在第9章的“下半部处理程序”一节介绍中断时详
细讨论。
任务队列是处理异步事件的重要资源,而且绝大多数的中断处理程序将它们的任
务延迟到任务队列被处理时执行。另外,有些任务队列是下半部处理程序,通过调用do_
bottom_half函数来处理。本章并不要求你理解下半部处理,但必要时我也会涉及到。
上面的数据结构中最重要的字段是routine和data。将要延迟的任务插入队列,
必须先设置好结构的这些字段,并把next和sync两个字段清零。结构中的sync标志位用
于避免同一任务被插入多次,这会破坏next指针。一旦任务被排入队列,该数据结构就
被认为为内核“拥有”了,不能再被修改。
与任务队列有关的其他数据结构还有task_queue,目前它实现为指向tq_struct
结构的指针;如果将来需要扩充task_queue,只要用typedef将该指针定义为其他符号就
可以了。
下面的列表汇总了所有可以对tq_struct结构进行的操作;所有的函数都是内联
的。
void queue_task(struct tq_struct *task, task_queue *list);
正如该函数的名字,本函数用于将任务排进队列中。它关闭了中断,避免了竞争,因此
正如该函数的名字,本函数用于将任务排进队列中。它关闭了中断,避免了竞争,因此
可以被模块中任一函数调用。
void queue_task_irq(struct tq_struct *task, task_queue *list);
与前者类似,但本函数只能由不可重入的函数调用(象中断处理程序,所以本函数的名字
带上了irq)。它比queue_task函数要快一些,因为它在排队前不关闭中断。如果你在一
个可重入的函数内调用本函数,由于没有屏蔽资源竞争,是很危险的。但是,本函数排
除了“运行时排队”的情形(也即将任务插入正在运行的那个任务的位置上)。
void queue_task_irq_off(struct tq_struct *task, task_queue *list);
本函数只能在中断已关闭的情况下调用。它比前两个函数要快,但没有防止象“并发排
队”和“运行时排队”这样的资源竞争。
void run_task_queue(struct tq_struct *task, task_queue *list);
run_task_queue函数用于运行累积在队列上的任务。除非你要声明和维护自己的任务队
列,否则不必调用本函数。
2.1.30版的内核已经不提供queue_task_irq和queue_task_irq_off这两个函数了
,被认为得不偿失。详情见第17章“最近的发展” 的“任务队列”一节。
在研讨任务队列的细节之前,最好还是先介绍一下内部的一些实现细节。任务队
列与相应的系统调用是异步执行的;这种异步执行特别需要注意,必须先介绍一下。
任务队列要在安全的时间内运行。这里安全的意思是在执行时没有什么特别严格
的要求。因为在处理任务队列时允许硬件中断,任务代码也不要求执行的非常快。但队
列中的函数执行得也不能太慢,毕竟在整个处理任务队列的期间,只有硬件中断才能被
系统处理。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -