ldd2第 6 章 时间流.docx
- 文档编号:18506436
- 上传时间:2023-08-18
- 格式:DOCX
- 页数:31
- 大小:39.13KB
ldd2第 6 章 时间流.docx
《ldd2第 6 章 时间流.docx》由会员分享,可在线阅读,更多相关《ldd2第 6 章 时间流.docx(31页珍藏版)》请在冰点文库上搜索。
ldd2第6章时间流
第6章时间流
至此,我们基本知道怎样编写一个功能完整的字符模块了。
现实中的设备驱动程序,除了实现必需的操作外还要做更多工作,如计时、内存管理,硬件访问等等。
幸好,内核中提供的许多机制可以简化驱动程序开发者的工作,我们将在后面几章陆续讨论驱动程序可以访问的一些内核资源。
本章,我们先来看看内核代码是如何对时间问题进行处理的。
按复杂程度递增排列,该问题包括:
理解内核时间机制
如何获得当前时间
如何将操作延迟指定的一段时间
如何调度异步函数到指定的时间后执行
内核中的时间间隔
我们首先要涉及的是时钟中断,操作系统通过时钟中断来确定时间间隔。
中断是异步事件,通常由外部硬件触发。
中断发生时,CPU停止正在进行的任务,转而执行另一段特殊的代码(即中断服务例程,又称ISR)来响应这个中断。
中断和ISR的实现将在第9章讨论。
时钟中断由系统计时硬件以周期性的间隔产生,这个间隔由内核根据HZ的值设定,HZ是一个与体系结构有关的常数,在文件
当前的Linux版本为大多数平台定义的HZ的值是100,某些平台上是1024,IA-64仿真器上是20。
驱动程序开发者不应使用任何特定的HZ值来计数,不管你的平台使用的是哪一个值。
当时钟中断发生时,变量jiffies的值就增加。
jiffies在系统启动时初始化为0,因此,jiffies值就是自操作系统启动以来的时钟滴答的数目,jiffies在头文件
为了保证jiffies溢出时内核仍能正常工作,人们已做了很多努力。
驱动程序开发人员通常不用考虑jiffies的溢出问题,知道有这种可能性就行了。
如果想改变系统时钟中断发生的频率,可以修改HZ值。
有人使用Linux处理硬实时任务,他们增加了HZ值以获得更快的响应时间,为此情愿忍受额外的时钟中断产生的系统开销。
总而言之,时钟中断的最好方法是保留HZ的缺省值,因为我们可以完全相信内核的开发者们,他们一定已经为我们挑选了最佳值。
处理器特有的寄存器
如果需要度量非常短的时间,或是需要极高的时间精度,可以使用与特定平台相关的资源,这是将时间精度的重要性凌驾于代码的可移植性之上的做法。
大多数较新的CPU都包含一个高精度的计数器,它每个时钟周期递增一次。
这个计数器可用于精确地度量时间。
由于大多数系统中的指令执行时间具有不可预测性(由于指令调度、分支预测、缓存等等),在运行具有很小时间粒度的任务时,使用这个时钟计数器是唯一可靠的计时方法。
为适应现代处理器的高速度,满足衡量性能指标的紧迫需求,同时由于CPU设计中的多层缓存引起的指令时间的不可预测性,CPU的制造商们引入了记录时钟周期这一测量时间的简单可靠的方法。
所以绝大多数现代处理器都包含一个随时钟周期不断递增的计数寄存器。
基于不同的平台,在用户空间,这个寄存器可能是可读的,也可能不可读;可能是可写的,也可能不可写;可能是64位的也可能是32位的。
如果是32位的,还得注意处理溢出的问题。
无论该寄存器是否可以置0,我们都强烈建议不要重置它,即使硬件允许这么做。
因为总可以通过多次读取该寄存器并比较读出数值的差异来完成要做的事,我们无须要求独占该寄存器并修改它的当前值。
最有名的计数器寄存器就是TSC(timestampcounter,时间戳计数器),从x86的Pentium处理器开始提供该寄存器,并包括在以后的所有CPU中。
它是一个64位寄存器,记录CPU时钟周期数,内核空间和用户空间都可以读取它。
包含了头文件
rdtsc(low,high);
rdtscl(low);
前一个宏原子性地把64位的数值读到两个32位变量中;后一个只把寄存器的低半部分读入一个32位变量,在大多数情况,这已经够用了。
举例来说,一个500MHz的系统使一个32位计数器溢出需8.5秒,如果要处理的时间肯定比这短的话,那就没有必要读出整个寄存器。
下面这段代码可以测量该指令自身的运行时间:
unsignedlongini,end;
rdtscl(ini);rdtscl(end);
printk("timelapse:
%li\en",end-ini);
其他一些平台也提供了类似的功能,在内核头文件中还有一个与体系结构无关的函数可以代替rdtsc,它就是get_cycles,是在2.1版的开发过程中引入的。
其原型是:
#include
cycles_tget_cycles(void);
在各种平台上都可以使用这个函数,在没有时钟周期记数寄存器的平台上它总是返回0。
cycles_t类型是能装入对应CPU单个寄存器的合适的无符号类型。
选择能装入单个寄存器的类型意味着,举例来说,get_cycles用于Pentium的时钟周期计数器时只返回低32位。
这种选择是明智的,它避免了多寄存器操作的问题,与此同时并未阻碍对该计数器的正常用法,即用来度量很短的时间间隔。
除了这个与体系结构无关的函数外,我们还将示例使用一段内嵌的汇编代码。
为此,我们来给MIPS处理器实现一个rdtscl函数,功能就象x86的一样。
这个例子之所以基于MIPS,是因为大多数MIPS处理器都有一个32位的计数器,在它们的内部“coprocessor0”中命名为register9寄存器。
为了从内核空间读取该寄存器,可以定义下面的宏,它执行“从coprocessor0读取”的汇编指令:
=======footnotebegins=============
nop指令是必需的,防止了编译器在指令mfc0之后立刻访问目标寄存器。
这种互锁(interlock)在RISC处理器中是很典型的,在延迟期间编译器仍然可以调度其它指令执行。
我们在这里使用nop,是因为内嵌汇编指令对编译器来说是个黑盒,不能进行优化。
=======footnoteends=============
#definerdtscl(dest)\e
__asm____volatile__("mfc0%0,$9;nop":
"=r"(dest))
通过使用这个宏,MIPS处理器就可以执行和前面所示用于x86的相同的代码了。
gcc内嵌汇编的有趣之处在于通用寄存器的分配使用是由编译器完成的。
这个宏中使用的%0只是“参数0”的占位符,参数0由随后的“作为输出(=)使用的任意寄存器(r)”定义。
该宏还说明了输出寄存器要对应于C表达式dest。
内嵌汇编的语法功能强大但也比较复杂,特别是在对各寄存器使用有限制的平台上更是如此,如x86系列。
完整的语法描述在gcc文档中提供,一般在info中就可找到。
这节展示的短小的C代码段已经在一个K7类的x86处理器和一个MIPSVR4181处理器(使用了刚才的宏)上运行过了。
前者给出的时间消耗为11时钟周期,后者仅为2时钟周期。
这是可以理解的,因为RISC处理器通常每时钟周期运行一条指令。
获取当前时间
内核一般通过jiffies值来获取当前时间。
该数值表示的是自最近一次系统启动到当前的时间间隔,它和设备驱动程序不怎么相关,因为它的生命期只限于系统的运行期(uptime)。
但驱动程序可以利用jiffies的当前值来计算不同事件间的时间间隔(比如在输入设备驱动程序中就用它来分辨鼠标的单双击)。
简而言之,利用jiffies值来测量时间间隔在大多数情况下已经足够了,如果还需要测量更短的时间,就只能使用处理器特有的寄存器了。
驱动程序一般不需要知道墙钟时间(指日常生活使用的时间),通常只有象cron和at这样用户程序才需要墙钟时间。
需要墙钟时间的情形是使用设备驱动程序的特殊情况,此时可以通过用户程序来将墙钟时间转换成系统时钟。
直接处理墙钟时间常常意味着正在实现某种策略,应该仔细审视一下是否该这样做。
如果驱动程序真的需要获取当前时间,可以使用do_gettimeofday函数。
该函数并不返回今天是本周的星期几或类似的信息;它是用秒或微秒值来填充一个指向structtimeval的指针变量,gettimeofday系统调用中用的也是同一变量。
do_gettimeofday的原型如下:
#include
voiddo_gettimeofday(structtimeval*tv);
源码中描述do_gettimeofday在许多体系结构上有“接近微秒级的分辨率”,然而实际精度是随不同的平台而变化的,在旧版本的内核中还会低些。
当前时间也可以通过xtime变量(类型为structtimeval)获得(但精度差些),但是,并不鼓励直接使用该变量,因为除非关闭中断,否则无法原子性地访问timeval变量的两个成员tv_sec和tv_usec。
在2.2版的内核中,一个快捷安全的获得时间的办法(可能精度会差些)是使用get_fast_time:
voidget_fast_time(structtimeval*tv);
获取当前时间的代码可见于jit(“JustInTime”)模块,源文件可以从O'Reilly公司的FTP站点获得。
jit模块将创建/proc/currentime文件,读取该文件将以ASCII码的形式返回三项:
由do_gettimeofday返回的当前时间
从xtime钟获得的当前时间
jiffies的当前值
我们选择用动态的/proc文件,是因为这样模块代码量会小些――不值得为返回三行文本而写一个完整的设备驱动程序。
Ifyouuse\*[PGN]cat\*[/PGN]toreadthefilemultiple
timesinlessthanatimertick,you'llseethedifferencebetween
xtimeand[I]do_gettimeofday[R],
reflectingthefactthatxtimeisupdatedless
frequently:
如果用cat命令在一个时钟滴答内多次读该文件,就会发现xtime和do_gettimeofday两者的差异了,xtime更新的次数不那么频繁:
morgana%cd/proc;catcurrentimecurrentimecurrentime
gettime:
846157215.937221
xtime:
846157215.931188
jiffies:
1308094
gettime:
846157215.939950
xtime:
846157215.931188
jiffies:
1308094
gettime:
846157215.942465
xtime:
846157215.941188
jiffies:
1308095
延迟执行
设备驱动程序经常需要将某些特定代码延迟一段时间后执行――通常是为了让硬件能完成某些任务。
这一节将介绍许多实现延迟的不同技术,哪种技术最好取决于实际环境中的具体情况。
我们将介绍所有的这些技术并指出各自的优缺点。
一件需要考虑的很重要的事情是所需的延迟长度是否多于一个时钟滴答。
较长的延迟可以利用系统时钟;较短的延迟通常必须通过软件循环来获得。
长延迟
如果想把执行延迟若干个时钟滴答,或者对延迟的精度要求不高(比如,想延迟整数数目的秒数),最简单的也是最笨的实现如下,也就是所谓的“忙等待”:
unsignedlongj=jiffies+jit_delay*HZ;
while(jiffies /*nothing*/; 这种实现当然要避免。 我们在这里提到它,只是因为读者可能某时需要运行这段代码,以便更好地理解其他的延迟技术。 还是先看看这段代码是如何工作的。 因为内核的头文件中jiffies被声明为volatile型变量,每次C代码访问它时都会重新读取它,因此该循环可以起到延迟的作用。 尽管也是“正确”的实现,但这个忙等待循环在延迟期间会锁住处理器,因为调度器不会中断运行在内核空间的进程。 更糟糕的是,如果在进入循环之前正好关闭了中断,jiffies值就不会得到更新,那么while循环的条件就永远为真,这时,你不得不按下那只大的红按钮(指电源按钮)。 这种延迟和下面的几种延迟方法都在jit模块中实现了。 由该模块创建的所有/proc/jit*文件每次被读取时都延迟整整1秒。 如果你想测试忙等待代码,可以读/proc/jitbusy文件,当该文件的read方法被调用时它将进入忙等待循环,延迟1秒;而象ddif=/proc/jitbusybs=1这样的命令每次读一个字符就要延迟1秒。 可以想见,读/proc/jitbusy文件会大大影响系统性能,因为此时计算机要到1秒后才能运行其他进程。 更好的延迟方法如下,它允许其他进程在延迟的时间间隔内运行,尽管这种方法不能用于硬实时任务或者其他对时间要求很严格的场合: while(jiffies schedule(); 这个例子和下面各例中的变量j应是延迟到达时的jiffies值,计算方法和忙等待一样。 这种循环(可以通过读/proc/jitsched文件来测试它)延迟方法还不是最优的。 系统可以调度其他任务;当前任务除了释放CPU之外不做任何工作,但是它仍在任务队列中。 如果它是系统中唯一的可运行的进程,它还会被运行(系统调用调度器,调度器选择同一个进程运行,此进程又再调用调度器,然后...)。 换句话说,机器的负载(系统中运行的进程平均数)至少为1,而idle进程(进程号为0,由于历史原因被称为“swapper”)绝不会被运行。 尽管这个问题看来无所谓,当系统空闲时运行idle进程可以减轻处理器负载,降低处理器温度,延长处理器寿命,如果是手提电脑,还能延长电池的寿命。 而且,延迟期间实际上进程是在执行的,因此延迟消耗的所有时间都是记在它的运行时间上的。 运行命令timecat/proc/jitsched就可以发现这一点。 另一种情况下,如果系统很忙,驱动程序等待的时间可能会比预计多得多。 一旦一个进程在调度时让出了处理器,无法保证以后的某个时间就能重新分配给它。 如果可接受的延迟时间有上限的话,用这种方式调用schedule,对驱动程序来说并不是一个安全的解决方案。 尽管有些毛病,这种循环延迟还是提供了一种有点“脏”但比较快的监视驱动程序工作的途径。 如果模块中的某个bug会锁死整个系统,则可在每个用于调试的printk语句后添加一小段延迟,这样可以保证在处理器碰到令人厌恶的bug而被锁死之前,所有的打印消息都能进入系统日志。 如果没有这样的延迟,这些消息只能进入内存缓冲区,但在klogd得到运行前系统可能已经被锁住了。 获得延迟的最好方法,是请求内核为我们实现延迟。 根据驱动程序是否在等待其他事件,有两种设置短期延迟的办法。 如果驱动程序使用等待队列等待某个事件,而你又想确保在一段时间后一定运行该驱动程序,可以使用sleep函数的超时版本,这在第5章“睡眠和唤醒”一节中已介绍过了: sleep_on_timeout(wait_queue_head_t*q,unsignedlongtimeout); interruptible_sleep_on_timeout(wait_queue_head_t*q, unsignedlongtimeout); 两种实现都能让进程在指定的等待队列上睡眠,而在超时期限(用jiffies表示)未到时的任何事件都会将其唤醒。 由此它们就实现了一种有上限的不会永远持续下去的睡眠。 注意超时值表示要等待的jiffies数量,而不是绝对的时间值。 这种方式的延迟可以在/proc/jitqueue的实现中看到: wait_queue_head_twait; init_waitqueue_head(&wait); interruptible_sleep_on_timeout(&wait,jit_delay*HZ); 在通常的驱动程序中,可以以下列两种方式重新获得执行: 在等待队列上调用一个wake_up,或者timout超时。 在这个特定实现中,没人会调用wake_up(毕竟其它代码根本就不知道这件事),所以进程总是因timeout超时而被唤醒。 这是一个完美有效的实现,不过,如果驱动程序无须等待其它事件,可以用一种更直接的方式获取延迟,即使用schedule_timeout: set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(jit_delay*HZ); 上述代码行(在/proc/jitself中实现)使进程进入睡眠直到指定时间。 schedule_timeout也是处理一个时间增量而不是一个jiffies的绝对值。 和前面一样,在从超时到进程实际被调度执行之间,可能会消耗一些毫无价值的额外时间。 短延迟 有时驱动程序需要非常短的延迟来和硬件同步。 此时,使用jiffies值无法达到目的。 这时就要用内核函数udelay和mdelay*。 ============footnotebegins=========== u表示希腊字母“mu”(μ),它代表“微”。 ============footnoteends=========== 它们的原型如下: #include voidudelay(unsignedlongusecs); voidmdelay(unsignedlongmsecs); 该函数在绝大多数体系结构上是作为内联函数编译的。 前者使用软件循环延迟指定数目的微秒数,后者使用udelay做循环,用于方便程序开发。 udelay函数里要用到BogoMips值: 它的循环基于整数值loops_per_second,这个值是在引导阶段计算BogoMips时得到的结果。 udelay函数只能用于获取较短的时间延迟,因为loops_per_second值的精度只有8位,所以,当计算更长的延迟时会积累出相当大的误差。 尽管最大能允许的延迟将近1秒(因为更长的延迟就要溢出),推荐的udelay函数的参数的最大值是取1000微秒(1毫秒)。 延迟大于1毫秒时可以使用函数mdelay。 要特别注意的是udelay是个忙等待函数(所以mdelay也是),在延迟的时间段内无法运行其他的任务,因此要十分小心,尤其是mdelay,除非别无他法,要尽量避免使用。 目前在支持大于几个微秒和小于1个时钟滴答的延迟时还是很低效的,但这通常不是个问题,因为延迟需要足够长,以便能够让人或者硬件注意到。 对人来说,百分之一秒的时间间隔是比较适合的精度,而1毫秒对硬件动作来说也足够长了。 mdelay在Linux2.0中并不存在,头文件sysdep.h弥补了这一缺陷。 任务队列 许多驱动程序需要将任务延迟到以后处理,但又不想借助中断。 Linux为此提供了三种方法: 任务队列、tasklet(从内核2.3.43开始)和内核定时器。 任务队列和tasklet的使用很灵活,可以或长或短地延迟任务到以后处理,在编写中断处理程序时非常有用,我们还将在第9章“Tasklet和底半部处理”一节中继续讨论。 内核定时器则用来调度任务在未来某个指定时间执行,将在本章的“内核定时器”一节中讨论。 使用任务队列或tasklet的一个典型情形是,硬件不产生中断,但仍希望提供阻塞型的读取。 此时需要对设备进行轮询,同时要小心地不使CPU负担过多无谓的操作。 将读进程以固定的时间间隔唤醒(例如,使用current->timeout变量)并不是个很好的方法,因为每次轮询需要两次上下文切换(一次是切换到读进程中运行轮询代码,另一次是返回执行实际工作的某个进程),而且通常来讲,恰当的轮询机制应该在进程上下文之外实现。 类似的情形还有象不时地给简单的硬件设备提供输入。 例如,有一个直接连接到并口的步进马达,要求该马达能一步步地移动,但马达每次只能移动一步。 在这种情况下,由控制进程通知设备驱动程序进行移动,但实际上,移动是在write返回后,才在周期性的时间间隔内一步一步进行的。 快速完成这类不定操作的恰当方法是注册任务在未来执行。 内核提供了对“任务队列”的支持,任务可以累积,而在运行队列时被“消耗”。 我们可以声明自己的任务队列,并且在任意时刻触发它,或者也可以将任务注册到预定义的任务队列中去,由内核来运行(触发)它。 这一节将首先概述任务队列,然后介绍预定义的任务队列,这使读者可以开始一些有趣的测试(如果出错也可能挂起系统),最后介绍如何运行自己的任务队列。 接着,我们来看看新的tasklet接口,在2.4内核中它在很多情况下取代了任务队列。 任务队列的本质 任务队列其实一个任务链表,每个任务用一个函数指针和一个参数表示。 任务运行时,它接受一个void*类型的参数,返回值类型为void,而指针参数可用来将一个数据结构传入函数,或者可以被忽略。 队列本身是一个结构(即任务)链表,并由声明和操纵它们的内核模块所拥有。 模块要全权负责这些数据结构的分配和释放,为此一般使用静态的数据结构。 队列元素由下面这个结构来描述,这段代码是直接从头文件 structtq_struct{ structtq_struct*next;/*linkedlistofactivebh's*/ intsync;/*mustbeinitializedtozero*/ void(*routine)(void*);/*functiontocall*/ void*data;/*argumenttofunction*/ }; 第一个注释中的bh指的是底半部(bottom-half)。 底半部是“中断处理程序的一半部”,我们将在第9章的“tasklet和底半部”一节中介绍中断时详细讨论。 现在,我们只要知道底半部是驱动程序实现的一种机制就可以了,它用于处理异步任务,这些任务通常比较大,不适于在处理硬件中断时完成。 本章并不要求你理解底半部处理,但必要时也会偶尔提及。 上面的数据结构中最重要的成员是routine和data。 为了将随后执行的任务排队,必须先设置好结构的这些成员,并把next和sync两个字段清零。 结构中的sync标志位由内核使用,以避免同一任务被插入多次,因为这会破坏next指针。 一旦任务被排队,该数据结构就被认为由内核“拥有”了,不能再被修改,直到任务开始运行。 与任务队列有关的其他数据结构还有task_queue,目前
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- ldd2第 时间流 ldd2 时间