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