软件for延时和定时器延时的不同,CPU占用率不同

  【IT168 技术文档】在开始步入Linux设備程序的神秘世界之前让我们从驱动程序开发人员的角度看几个内核构成要素,熟悉一些基本的内核概念我们将学习内核定时器、同步机制以及分配方法。不过我们还是得从头开始这次探索之旅。因此本章要先浏览一下内核发出的启动信息,然后再逐个讲解一些有意思的点

   图2-1显示了基于x86计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR)接下来MBR中的代码查看分区表并 从活动分区讀取GRUB、LILO或SYSLINUX等引导装入程序,之后引导装入程序会加载压缩后的内核映像并将控制权传递给它内核取得控制权后,会 将自身解压缩并投入運转

  基于x86的处理器有两种操作模式:实模式和保护模式。在实模式下用户仅可以使用1 MB内存,并且没有任何保护保护模式要复杂嘚多,用户可以使用更多的高级功能(如分页)必须中途将实模式切换为保护模式。但是这种切换是单向的,即不能从保护模式再切换回實模式

   内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下init/main.c文件(上一章修改的源文件)中的start_kernel() 函数start_kernel()函数首先会初始囮CPU子系统,之后让内存和进程管理系统就位接下来启动外部总线和I/O设备,最后一步是激活初始 化(init)程序它是所有Linux进程的父进程。初始化進程执行启动必要的内核服务的用户空间脚本并且最终派生控制台终端程序以及显示登录 (login)提示。

图2-1 基于x86硬件上的Linux的启动过程

  本节內的3级标题都是图2-2中的一条打印信息这些信息来源于基于x86的电脑的Linux启动过程。如果在其他体系架构上启动内核消息以及语义可能会有所不同。

  内核会解析从BIOS中读取到的系统内存映射并率先将以下信息打印出来:

   实模式下的初始化代码通过使用BIOS的int 0x15服务并执行0xe820号函数(即上面的BIOS-e820字符串)来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存内核将 随后使用这些信息创建其可用的内存池。在附录B的B.1节我们会对BIOS提供的内存映射问题进行更深入的讲解。

图2-2 内核启动信息

  896 MB以内的常规的可被寻址的内存区域被称作低端内存内存分配函数kmalloc()就是从该区域分配内存的。高于896 MB的内存区域被称为高端内存只有在采用特殊的方式进行映射后才能被访问。

  茬启动过程中内核会计算并显示这些内存区内总的页数。

   Linux的引导装入程序通常会给内核传递一个命令行命令行中的参数类似于传遞给C程序中main()函数的argv[]列表,唯一的不同在于它们是 传递给内核的可以在引导装入程序的配置文件中增加命令行参数,当然也可以在运行過程中修改引导装入程序的提示行[1]。如果使用的是GRUB这个引导

   命令行参数将影响启动过程中的代码执行路径举一个例子,假设某命令荇参数为bootmode如果该参数被设置为1,意味着你希望在启动过程中打印一 些调试信息并在启动结束时切换到runlevel的第3级(初始化进程的启动信息打印後就会了解runlevel的含义);如果bootmode参数被设 置为0意味着你希望启动过程相对简洁,并且设置runlevel为2既然已经熟悉了init/main.c文件,下面就在该文件中增加如下修改:

  请重新编译内核并尝试运行新的修改

  在启动过程中,内核会计算处理器在一个jiffy时间内运行一个内部的延迟循环的次数jiffy嘚含义是系统定时器2个连续的节拍之间的间隔。正如所料该计算必须被校准到所用CPU的处理速度。校准的结果被在称为loops_per_jiffy的内核变量中使鼡loops_per_jiffy的一种情况是某设备驱动程序希望进行小的微秒级别的延迟的时候。

   为了理解延迟—循环校准代码让我们看一下定义于init/calibrate.c文件中的calibrate_ delay()函数。该函数灵活地使用整型运算得到了浮点的精度如下的代码片段(有一些注释)显示了该函数的开始部分,这部分用于得到一个 loops_per_jiffy的粗略徝:

   上述代码首先假定loops_per_jiffy大于4096这可以转化为处理器速度大约为每秒100万条指令,即1 MIPS接下来,它等待jiffy被刷新(1个新的节拍的开始)并开始運行延迟循环__delay(loops_per_jiffy)。如果这个延迟 循环持续了1个jiffy以上将使用以前的loops_per_jiffy值(将当前值右移1位)修复当前loops_per_jiffy的最高位;否 则,该函数继续通过左移loops_per_jiffy值来探测絀其最高位在内核计算出最高位后,它开始计算低位并微调其精度:

   上述代码计算出了延迟循环跨越jiffy边界时loops_per_jiffy的低位值这个被校准嘚值可被用于获取BogoMIPS(其实它是一个并 非科学的处理器速度指标)。可以使用BogoMIPS作为衡量处理器运行速度的相对尺度在1.6G Hz 基于Pentium M的电脑上,根据前述啟动过程的打印信息循环校准的结果是:loops_per_jiffy的值为2394935。获得BogoMIPS的方式如下:

  由于Linux内核支持多种硬件平台启动代码会检查体系架构相关的bug。其中一项工作就是验证停机(HLT)指令

   x86处理器的HLT指令会将CPU置入一种低功耗睡眠模式,直到下一次硬件中断发生之前维持不变当内核想讓CPU进入空闲状态时(查看 arch/x86/kernel/process_32.c文件中定义的cpu_idle()函数),它会使用HLT指令对于有问题的CPU而言,命令 行参数no-hlt可以禁止HLT指令如果no-hlt被设置,在空闲的时候內核会进行忙等待而不是通过HLT给CPU降温。

  Linux套接字(socket)层是用户空间应用程序访问各种协议的统一接口每个协议通过include/linux/socket.h文件中定义的分配给它嘚独一无二的系列号注册。上述打印信息中的Family

  启动过程中另一个常见的注册协议系列是AF_NETLINK(Family 16)网络链接套接字提供了用户进程和内核的 方法。通过网络链接套接字可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用 法列表)对于此类任务而言,网络链接套接字比系统调用更合适因为前者具有采用异步机制、更易于实现和可动态链接的优点。

  内核中经常使能的另一个协议系列是AF_Unix或Unix-domain套接芓X Windows等程序使用它们在同一个系统上进行进程间通信。

   initrd是一种由引导装入程序加载的常驻内存的虚拟磁盘映像在内核启动后,会将其挂载为初始根文件系统这个初始根文件系统中存放着挂载实际根文 件系统磁盘分区时所依赖的可动态连接的模块。由于内核可运行于各种各样的存储控制器硬件平台上把所有可能的磁盘驱动程序都直接放进基本的内核映像中并不 可行。你所使用的系统的存储设备的驱動程序被打包放入了initrd中在内核启动后、实际的根文件系统被挂载之前,这些驱动程序才被加载使用 mkinitrd命令可以创建一个initrd映像。

  2.6内核提供了一种称为initramfs的新功能它在几个方面较 initrd更为优秀。后者模拟了一个磁盘(因而被称为initramdisk或initrd)会带来Linux块I/O子系统的开销(如缓冲);前者 基本上如同┅个被挂载的文件系统一样,由自身获取缓冲(因此被称作initramfs)

  不同于initrd,基于页缓冲建立的 initramfs如同页缓冲一样会动态地变大或缩小从而减尐了其内存消耗。另外initrd要求你的内核映像包含initrd所使用的文件系统(例 如,如果initrd为EXT2文件系统内核必须包含EXT2驱动程序),然而initramfs不需要文件系统支持再者,由于initramfs只 是页缓冲之上的一小层因此它的代码量很小。

  用户可以将初始根文件系统打包为一个cpio压缩包[1]并通过initrd=命令 行参 數传递给内核。当然也可以在内核配置过程中通过INITRAMFS_SOURCE选项直接编译进内核。对于后一种方式而言用户可以提供cpio压缩包 的文件名或者包含initramfs嘚目录树。在启动过程中内核会将文件解压缩为一个initramfs根文件系统,如果它找到了/init它就会执 行该顶层的程序。这种获取初始根文件系统嘚方法对于嵌入式系统而言特别有用因为在嵌入式系统中系统资源非常宝贵。使用mkinitramfs可以创建一 个initramfs映像查看文档Documentation/filesystems/ramfs- rootfs-initramfs.txt可获得更多信息。

  茬本例中我们使用的是通过initrd=命令行参数向内核传递初始根文件 系统cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后内核将释放该压缩包所占据的内存(本例中为387 KB)并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请

  在嵌入式系统开发过程Φ,initrd和initramfs有时候也可被用作嵌入式设备上实际的根文件系统

   I/O调度器的主要目标是通过减少磁盘的定位次数来增加系统的吞吐率。在磁盤定位过程中磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的 延迟2.6内核提供了4种不同的I/O调度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。从上述內核打印信息可以看出本例将Anticipatory 设置为了默认的I/O调度器。

  启动过程的下一阶段会初始化I/O总线和外围控制器内核会通过遍历PCI总线来探測PCI硬件,接下来再初始化其他的I/O子系统从图2-3中我们会看到SCSI子系统、USB控制器、芯片(855北桥芯片组信息中的一部分)、串行端口(本例中为8250 UART)、PS/2和、、ramdisk、loopback设备、IDE控制器(本例中为ICH4南桥芯片组中的一部分)、触控板、以太网控制器(本例中为e1000)以及PCMCIA控制器初始化的启动信息。图2-3中 符号指向的为I/O设備的标识(ID)

图2-3 在启动过程中初始化总线和外围控制器

  本书会以单独的章节讨论大部分上述驱动程序子系统,请注意如果驱动程序以模块的形式被动态链接到内核其中的一些消息也许只有在内核启动后才会被显示。

   EXT3文件系统已经成为Linux事实上的文件系统EXT3在退役的EXT2攵件系统基础上增添了日志层,该层可用于崩溃后文件系统的快速恢复它 的目标是不经由耗时的文件系统检查(fsck)操作即可获得一个一致的攵件系统。EXT2仍然是新文件系统的工作引擎但是EXT3层会在进行实际的磁盘 改变之前记录文件交互的日志。EXT3向后兼容于EXT2因此,你可以在你现存的EXT2文件系统上加上EXT3或者由EXT3返回到EXT2文件系 统

  EXT3会启动一个称为kjournald的内核辅助线程(在接下来的一章中将深入讨论内核线程)来完成日志功能。在EXT3投入运转以后内核挂载根文件系统并做好“业务”上的准备:

  所有Linux进程的父进程init是内核完成启动序列后运行的第1个程序。在init/main.c的朂后几行内核会搜索一个不同的位置以定位到init:

  init会接受/etc/inittab的指引。它首先执行/etc/rc.sysinit中的系统初始化脚本该脚本的一项最重要的职责就是噭活对换(swap)分区,这会导致如下启动信息被打印:

   让我们来仔细看看上述这段话的意思Linux用户进程拥有3 GB的虚拟地址空间(见2.7节),构成“工莋集”的页被保存在RAM中但是,如果有太多程序需要内存资源内核会释放一些被使用了的RAM页面并将其 存储到称为对换空间(swap space)的磁盘分区中。根据经验法则对换分区的大小应该是RAM的2倍。在本例中对换空间位于/dev/hda6这个磁盘分区,其大小为1 552

3这条信息的时候init就已经开始执行/etc/rc.d/rc3.d/目录Φ的脚本了。这些脚本会启动动态设备命名子系统(第4章中将讨论 udev)并加载网络、音频、存储设备等驱动程序所对应的内核模块:

  最后,init发起虚拟控制台终端你现在就可以登录了。

  2.2 内核模式和用户模式

  MS-DOS等在单一的CPU模式下运行但是一些类Unix的操作系统则使用了雙模式,可以有效地实现时间共享在Linux机器上,CPU要么处于受信任的内核模式要么处于受限制的用户模式。除了内核本身处于内核模式以外所有的用户进程都运行在用户模式之中。

  内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和I/O空间如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码发出请求另外,用户模式的代码允许发生缺页而內核模式的代码则不允许。

  在2.4和更早的内核中仅仅用户模式的进程可以被上下文切换出局,由其他进程抢占除非发生以下两种情況,否则内核模式代码可以一直独占CPU:

  (2) 发生中断或异常

  2.6内核引入了内核抢占,大多数内核模式的代码也可以被抢占

  2.3 进程上下文和中断上下文

   内核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上 下文异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下攵中断上下文和进程上下文不可能同时发生。

  运行于进程上下文的内核代码是可抢占的但进程上下文则会一直运行至结束,不会被抢占因此,内核会限制中断上下文的工作不允许其执行如下操作:

  (1) 进入睡眠状态或主动放弃CPU;

  (2) 占用互斥体;

  (3) 执行耗时的任務;

  (4) 访问用户空间虚拟内存。

  本书4.2节会对中断上下文进行更深入的讨论

  2.4 内核定时器

   内核中许多部分的工作都高度依赖於时间信息。Linux内核利用硬件提供的不同的定时器以支持忙等待或睡眠等待等时间相关的服务忙等待时,CPU会不 断运转但是睡眠等待时,進程将放弃CPU因此,只有在后者不可行的情况下才考虑使用前者。内核也提供了某些便利可以在特定的时间之后调度某函数运 行。

  我们首先来讨论一些重要的内核定时器变量(jiffies、HZ和xtime)的含义接下来,我们会使用Pentium时间戳计数器(TSC)测量基于Pentium的系统的运行次数之后,我们也汾析一下Linux怎么使用实时钟(RTC)

  系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数对应着内核变量HZ。选择合适嘚HZ值需要权衡HZ值大,定时器间隔时间就小因此进程调度的准确性会更高。但是HZ值越大也会导致开销和消耗更多,因为更多的处理器周期将被耗费在定时器中断上下文中

HZ的值取决于体系架构。在x86系统上在2.4内核中,该值默认设置为100;在2.6内核中该值变为1000;而在2.6.13中,它叒被降低到了250在基于ARM的平台上,2.6内核将HZ设置为100在目前的内核中,可以在编译内核时通过配置菜单选择一个HZ值该选项的默认值取决于體系架构的版本。
2.6.21内核支持无节拍的内核(CONFIG_NO_HZ)它会根据系统的负载动态触发定时器中断。无节拍系统的实现超出了本章的讨论范围不洅详述。

  jiffies变量记录了系统启动以来系统定时器已经触发的次数。内核每秒钟将jiffies变量增加HZ次因此,对于HZ值为100的系统1个jiffy等于10ms,而对於HZ为1000的系统1个jiffy仅为1ms。

  为了更好地理解HZ和jiffies变量请看下面的取自IDE驱动程序(drivers/ide/ide.c)的代码片段。该段代码会一直轮询磁盘驱动器的忙状态:

   如果忙条件在3s内被清除上述代码将返回SUCCESS,否则返回-EBUSY。3*HZ是3s内的jiffies数量计算出来的超时 jiffies + 3*HZ将是3s超时发生后新的jiffies值。time_after()的功能是将目前的jiffies值与請求的超时时间对比检测溢出。类似函数

  jiffies被定义为volatile类型它会告诉编译器不要优化该变量的存取代码。这样就确保了每个节拍发生嘚定时器中断处理程序都能更新jiffies值并且循环中的每一步都会重新读取jiffies值。

   假定jiffies值为100032位的jiffies会在大约50天的时间内溢出。由于系统的运荇时间可以比该时间长许多倍因此,内核提供了另一 个变量jiffies_64以存放64位(u64)的jiffies链接器将jiffies_64的低32位与32位的jiffies指向同一个地址。

  2.4.2 长延时

  在內核中以jiffies为单位进行的延迟通常被认为是长延时。一种可能但非最佳的实现长延时的方法是忙等待实现忙等待的函数有“占着茅坑不拉屎”之嫌,它本身不利用CPU进行有用的工作同时还不让其他程序使用CPU。如下代码将占用CPU 1秒:

  实现长延时的更好方法是睡眠等待而不昰忙等待在这种方式中,本进程会在等待时将处理器出让给其他进程schedule_timeout()完成此功能:

   这种延时仅仅确保超时较低时的精度。由于只囿在时钟节拍引发的内核调度才会更新jiffies所以无论是在内核空间还是在用户空间,都很难使超时的精 度比HZ更大了另外,即使你的进程已經超时并可被调度但是调度器仍然可能基于优先级策略选择运行队列的其他进程[1]。

  这种长延时技术仅仅适用于进程上下文睡眠等待不能用于中断上下文,因为中断上下文不允许执行 schedule() 或睡眠(4.2节给出了中断上下文可以做和不能做的事情)在中断中进行短时间的忙等待是鈳行的,但是进行长时间的忙等则被认为不可赦免的罪行在中断禁 止时,进行长时间的忙等待也被看作禁忌

  为了支持在将来的某時刻进行某项工作,内核也提供了定时器API可以通过 init_timer()动态定义一个定时器,也可以通过DEFINE_TIMER()静态创建定时器然后,将处理函数的地址和参数綁定给一个 timer_list并使用add_timer()注册它即可:

  上述代码只会让定时器运行一次。如果想让timer_func()函数周期性地执行需要在timer_func()加上相关代码,指定其在下佽超时后调度自身:

  clock_settime()和clock_gettime()等用户空间函数可用于获得内核定时器服务用户应用程序可以使用setitimer()和getitimer()来控制一个报警信号在特定的超时后发苼。

  2.4.3 短延时

  在内核中小于jiffy的延时被认为是短延时。这种延时在进程或中断上下文都可能发生由于不可能使用基于jiffy的方法实現短延时,之前讨论的睡眠等待将不再能用于短的超时这种情况下,唯一的解决途径就是忙等待

  实现短延时的内核API包括mdelay()、udelay()和ndelay(),分別支持毫秒、微秒和纳秒级的延时这些函数的实际实现取决于体系架构,而且也并非在所有平台上都被完整实现

   忙等待的实现方法是测量处理器执行一条指令的时间,为了延时执行一定数量的指令。从前文可知内核会在启动过程中进行测量并将该值存储在 loops_per_jiffy变量Φ。短延时API就使用了loops_per_jiffy值来决定它们需要进行循环的数量为了实现握手进程中1微秒

   时间戳计数器(TSC)是Pentium兼容处理器中的一个计数器,它记錄自启动以来处理器消耗的时钟周期数由于TSC随着处理器周期速率的比例的变 化而变化,因此提供了非常高的精确度TSC通常被用于剖析和監测代码。使用rdtsc指令可测量某段代码的执行时间其精度达到微秒级。TSC的节拍可 以被转化为秒方法是将其除以CPU时钟速率(可从内核变量cpu_khz读取)。

  在如下代码片段中low_tsc_ticks和high_tsc_ticks分别包含了TSC的低32位和高32位。低32位可能在数秒内溢出(具体时间取决于处理器速度)但是这已经用于许多代码嘚剖析了:

在2.6.21内核中,针对高精度定时器的支持(CONFIG_HIGH_RES_TIMERS)已经被融入了内核它使用了硬件特定的高速定时器来提供对nanosleep()等API高精度的支持。在基於Pentium的机器上内核借助TSC实现这一功能。

  2.4.5 实时钟

   RTC在非易失性存储器上记录绝对时间在x86 PC上,RTC位于由电池供电[1]的互补金属氧化物半導体(CMOS)存储器的顶部从第5章的图5-1可以看出传统PC体系架构中CMOS的位置。在 嵌入式系统中RTC可能被集成到处理器中,也可能通过I2C或SPI总线在外部连接见第8章。

  使用RTC可以完成如下工作:

  (1) 读取、设置绝对时间在时钟更新时产生中断;

  (2) 产生频率为2~8192 Hz之间的周期性中断;

  (3) 设置报警信号。

   许多应用程序需要使用绝对时间[或称墙上时间(wall time)]jiffies是相对于系统启动后的时间,它不包含墙上时间内核将墙上时间记录茬xtime变量中,在启动过程中会根据从RTC读取到 的目前的墙上时间初始化xtime,在系统停机后墙上时间会被写回RTC。你可以使用do_gettimeofday()读取墙上时间其朂高精度由硬 件决定:

  用户空间也包含一系列可以访问墙上时间的函数,包括:

  (1) time()该函数返回日历时间,或从新纪元(1970年1月1日00:00:00)以来經历的秒数;

  (4) gettimeofday()如果你的平台支持,该函数将以微秒精度返回日历时间

  用户空间使用RTC的另一种途径是通过字符设备/dev/rtc来进行,同一時刻只有一个进程允许返回该字符设备

  在第5章和第8章,本书将更深入讨论RTC驱动程序另外,在第19章给出了一个使用/dev/rtc以微秒级精度执荇周期性工作的应用程序示例

  2.5 内核中的并发

  随着多核笔记本电脑时代的到来,对称多处理器(SMP)的使用不再被限于高科技用户SMP囷内核抢占是多线程执行的两种场景。多个线程能够同时操作共享的内核数据结构因此,对这些数据结构的访问必须被串行化

  接丅来,我们会讨论并发访问情况下保护共享内核资源的基本概念我们以一个简单的例子开始,并逐步引入中断、内核抢占和SMP等复杂概念

  2.5.1 自旋锁和互斥体

  访问共享资源的代码区域称作临界区。自旋锁(spinlock)和互斥体(mutexmutual exclusion的缩写)是保护内核临界区的两种基本机制。我们逐個分析

  自旋锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转直到第1个线程释放自旋锁。注意:这里所说的线程不是内核线程而是执行的线程。

  下面的例子演示了自旋锁的基本用法:

   与自旋锁不同的是互斥体在進入一个被占用的临界区之前不会原地打转,而是使当前线程进入睡眠状态如果要等待的时间较长,互斥体比自旋锁更合适因为 自旋鎖会消耗CPU资源。在使用互斥体的场合多于2次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠而当其被唤醒时,咜需要被切换 回来

  因此,在很多情况下决定使用自旋锁还是互斥体相对来说很容易:

  (1) 如果临界区需要睡眠,只能使用互斥体因为在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的;

  (2) 由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此在中断处理函数中,只能使用自旋锁(第4章将介绍更多的关于中断上下文的限制。)

  下面的例子演示了互斥体使用的基本方法:

  为了论证并发保护的用法我们首先从一个仅存在于进程上下文的临界区开始,并以下面的顺序逐步增加复杂性:

  (1) 非抢占内核单CPU情况下存在于进程上下文的临界区;

  (2) 非抢占内核,单CPU情况下存在于进程和中断上下文的临界区;

  (3) 可抢占内核单CPU情况下存在于进程和中断上下文的临界区;

  (4) 可抢占内核,SMP情况下存在于进程和中断上下文的临界区

  互斥体接口代替了旧的信号量接口(semaphore)。互斥体接ロ是从-rt树演化而来的在2.6.16内核中被融入主线内核。

  尽管如此但是旧的信号量仍然在内核和驱动程序中广泛使用。信号量接口的基本鼡法如下:

  1. 案例1:进程上下文单CPU,非抢占内核

  这种情况最为简单不需要加锁,因此不再赘述

  2. 案例2:进程和中断上下文,单CPU非抢占内核

  在这种情况下,为了保护临界区仅仅需要禁止中断。如图2-4所示假定进程上下文的执行单元A、B以及中断上下文的執行单元C都企图进入相同的临界区。

图2-4 进程和中断上下文进入临界区

   由于执行单元C总是在中断上下文执行它会优先于执行单元A和B,因此它不用担心保护的问题。执行单元A和B也不必关心彼此会被互相打断因为内核是 非抢占的。因此执行单元A和B仅仅需要担心C会在咜们进入临界区的时候强行进入。为了实现此目的它们会在进入临界区之前禁止中断:

   但是,如果当执行到Point A的时候已经被禁止local_irq_enable()将產生副作用,它会重新使能中断而不是恢复之前的中断状态。可以这样修复它:

  不论Point A的中断处于什么状态上述代码都将正确执行。

  3. 案例3:进程和中断上下文单CPU,抢占内核

   如果内核使能了抢占仅仅禁止中断将无法确保对临界区的保护,因为另一个处于进程上下文的执行单元可能会进入临界区重新回到图2-4,现在除了C以 外,执行单元A和B必须提防彼此显而易见,解决该问题的方法是在进叺临界区之前禁止内核抢占、中断并在退出临界区的时候恢复内核抢占和中断。因此执 行单元A和B使用了自旋锁API的irq变体:

   我们不需偠在最后显示地恢复Point A的抢占状态,因为内核自身会通过一个名叫抢占计数器的变量维护它在抢占被禁止时(通过调用preempt_disable()),计数器值会增加;在 搶占被使能时(通过调用preempt_enable())计数器值会减少。只有在计数器值为0的时候抢占才发挥作用。

  4. 案例4:进程和中断上下文SMP机器,抢占内核

  到目前为止讨论的场景中自旋锁原语发挥的作用仅限于使能和禁止抢占和中断,时间的锁功能并未被完全编译进来在SMP机器内,锁邏辑被编译进来而且自旋锁原语确保了SMP性。SMP使能的含义如下:

   在SMP系统上获取自旋锁时,仅仅本CPU上的中断被禁止因此,一个进程仩下文的执行单元(图2-4中的执行单元A)在一个CPU上运行的同时一 个中断处理函数(图2-4中的执行单元C)可能运行在另一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区中 断上下文需要调用spin_lock()/spin_unlock():

  除了有irq变体以外,自旋锁也有底半部(BH)变体在锁被获取的时候,spin_lock_bh()会禁止底半部而spin_unlock_bh()则会在锁被释放时重新使能底半部。我们将在第4章讨论底半部

些互斥体。它也合并了一些高精度的定时器数个-rt功能已经被融入了主线内核。详细的文档见http://rt.wiki.kernel.org/

  为了提高性能,内核也定义了一些针对特定环境的特定的锁原语使能适用于代码执行場景的互斥机制将使代码更高效。下面来看一下这些特定的互斥机制

  2.5.2 原子操作

  原子操作用于执行轻量级的、仅执行一次的操莋,例如修改计数器、有条件的增加值、设置位等原子操作可以确保操作的串行化,不再需要锁进行并发访问保护原子操作的具体实現取决于体系架构。

  为了在释放内核网络缓冲区(称为skbuff)之前检查是否还有余留的数据引用定义于net/core/skbuff.c文件中的skb_release_data()函数将进行如下操作:

  原子操作的使用将确保数据引用计数不会被这两个执行单元“蹂躏”。它也消除了使用锁去保护单一整型变量的争论

  2.5.3 读—写锁

  另一个特定的并发保护机制是自旋锁的读—写锁变体。如果每个执行单元在访问临界区的时候要么是读要么是写共享的数据结构但是咜们都不会同时进行读和写操作,那么这种锁是最好的选择允许多个读线程同时进入临界区。读自旋锁可以这样定义:

  但是如果┅个写线程进入了临界区,那么其他的读和写都不允许进入写锁的用法如下:

   net/ipx/ipx_route.c中的IPX路由代码是使用读—写锁的真实示例。一个称作ipx_routes_lock嘚读—写锁将保护IPX 路由表的并发访问要通过查找路由表实现包转发的执行单元需要请求读锁。需要添加和删除路由表中入口的执行单元必须获取写锁由于通过读路由表的情况比更 新路由表的情况多得多,使用读—写锁提高了性能

  2.6内核引入的顺序锁(seqlock)是一种支持写多於读的读—写锁。在一个变量的写操作比读操作多得多的 情况 下这种锁非常有用。前文讨论的jiffies_64变量就是使用顺序锁的一个例子写线程鈈必等待一个已经进入临界区的读,因此读线程也许会发现它们 进入临界区的操作失败,因此需要重试:

   2.6内核还引入了另一种称为讀—复制—更新(RCU)的机制该机制用于提高读操作远多于写操作时的性能。其基本理念是读线程不需要加锁但是写线程 会变得更加复杂,咜们会在数据结构的一份副本上执行更新操作并代替读者看到的指针。为了确保所有正在进行的读操作的完成原子副本会一直被保持箌所有 CPU上的下一次上下文切换。使用RCU的情况很复杂因此,只有在确保你确实需要使用它而不是前文的其他原语的时候才适宜选择它。 include/linux/ rcupdate.h攵件中定义了RCU的数据结构和接口函数Documentation/RCU/*提供了丰富的文档。

   fs/dcache.c文件中包含一个RCU的使用示例在Linux中,每个文件都与一个目录入口信息(dentry结构體)、元数据信息(存放在 inode中)和实际的数据(存放在数据块中)关联每次操作一个文件的时候,文件路径中的组件会被解析相应的dentry会被获取。為了加速未来的操 作dentry结构体被缓存在称为dcache的数据结构中。任何时候对dcache进行查找的数量都远多于dcache的更新操作,因此对 dcache的访问适宜用RCU原語进行保护。

  由于难于重现并发相关的 问 题通常非常难调试。在编译和测试代码的时候使能SMP(CONFIG_SMP)和抢占(CONFIG_PREEMPT)是一种很好的理念即便你的产品将 运行在单CPU、禁止抢占的情况下。在Kernel hacking下有一个称为Spinlock and rw-lock

  在访问共享资源之前忘记加锁就会出现常见的并发问题这会导致一些不同的执荇单元杂乱地“竞争”。这种问题(被称作“竞态”)可能会导致一些其他的行为

  在某些代码路径里忘记了释放锁也会出现并发问题,這会导致死锁为了理解这个问题,让我们分析如下代码:

  if (error)语句成立的话任何要获取mylock的线程都会死锁,内核也可能因此而冻结

  如果在写完代码的数月或数年以后首次出现了问题,回过头来调试它将变得更为棘手(在21.3.3节有一个相关的调试例子。)因此为了避免遭遇这种不快,在设计架构的时候就应该考虑并发逻辑。

  proc文件系统(procfs)是一种虚拟的文件系统它创建内核内部的视窗。浏览procfs时看到的数據是在内核运行过程中产生的procfs中的文件可被用于配置内核参数、查看内核结构体、从设备驱动程序中收集统计信息或者获取通用的系统信息。

  procfs是一种虚拟的文件系统这意味着驻留于procfs中的文件并不与物理存储设备如等关联。相反这些文件中的数据由内核中相应的入ロ点按需动态创建。因此procfs中的文件大小都显示为0。procfs通常在启动过程中挂载在/proc目录通过运行mount命令可以看出这一点。

些内核参数例如,通过向/proc/sys/kernel/printk文件回送一个新的值可以改变内核printk日志的级别。许多实用程序(如 ps)和系统性能监视工具(如sysstat)就是通过驻留于/proc中的文件来获取信息的

  2.6内核引入的seq文件简化了大的procfs操作。附录C对此进行了描述

  一些设备驱动程序必须意识到内存区的存在,另外许多驱动程序需要內存分配函数的服务。本节我们将简要地讨论这两点

  内核会以分页形式组织物理内存,而页大小则取决于具体的体系架构在基于x86嘚机器上,其大小为4096B物理内存中的每一页都有一个与之对应的struct page(定义在include/linux/ mm_types.h文件中):

   在32位x86系统上,默认的内核配置会将4 GB的地址空间分成给鼡户空间的3 GB的虚拟内存空间和给内核空间的1 GB的空间(如图2-5所示)这导致内核能处理的处理内存有1 GB的限制。现实情况是限制为896 MB,因为地址空間的128 MB已经被内核数据结构占据通过改变3 GB/1 GB的分割线,可以放宽这个限制但是由于减少了用户进程虚拟地址空间的大小,在内存密集型的應用程序中可能会出现一些问题

图2-5 32位PC系统上默认的地址空间分布

   内核中用于映射低于896 MB物理内存的地址与物理地址之间存在线性偏迻;这种内核地址被称作逻辑地址。在支持“高端内存”的情况下在通过特定的方式映射这些区域产生对应的虚拟 地址后,内核将能访问超过896 MB的内存所有的逻辑地址都是内核虚拟地址,而所有的虚拟地址并非一定是逻辑地址

  因此,存在如下的内存区

  (1) ZONE_DMA(小于16 MB),该區用于直接内存访问(DMA)由于传统的ISA设备有24条地址线,只能访问开始的16 MB因此,内核将该区献给了这些设备

  (2) ZONE_NORMAL(16~896 MB),常规地址区域,也被称莋低端内存用于低端内存页的struct page结构中的“虚拟”字段包含了对应的逻辑地址。

   (3) ZONE_HIGH(大于896 MB)仅仅在通过kmap()映射页为虚拟地址后才能访问。(通過kunmap()可去除映射)相应的内核地址为虚拟地址而非逻辑地址。如果相应的页 未被映射用于高端内存页的struct page结构体的“虚拟”字段将指向NULL。

  kmalloc()是一个用于从ZONE_NORMAL区域返回连续内存的内存分配函数其原型如下:

  (1) GFP_KERNEL,被进程上下文用来分配内存如果指定了该标志,kmalloc()将被允许睡眠以等待其他页被释放。

  (2) GFP_ATOMIC被中断上下文用来获取内存。在这种模式下kmalloc()不允许进行睡眠等待,以获得空闲页因此GFP_ATOMIC分配成功的可能性比用GFP_KERNEL低。

  由于kmalloc()返回的内存保留了以前的内容将它暴露给用户空间可到会导致安全问题,因此我们可以使用kzalloc()获得被填充为0的内存

  如果需要分配大的内存缓冲区,而且也不要求内存在物理上有联系可以用vmalloc()代替kmalloc():

  count是要请求分配的内存大小。该函数返回内核虚擬地址

   vmalloc()需要比kmalloc()更大的分配空间,但是它更慢而且不能从中断上下文调用。另外不能用vmalloc()返回的物理上不连 续的内存执行DMA。在设备咑开时高性能的网络驱动程序通常会使用vmalloc()来分配较大的描述符环行缓冲区。

  内核还提供了一些更复杂的内存分配技术包括后备缓沖区(look aside buffer)、slab和mempool;这些概念超出了本章的讨论范围,不再细述

  2.8 查看源代码

  内存启动始于执行arch/x86/boot/目录中的实模式汇编代码。查看arch/x86/kernel/setup_32.c文件可以看出保护模式的内核怎样获取实模式内核收集的信息

  内存管理源代码存放在顶层mm/目录中。

  表2-1给出了本章中主要的数据结构以及其在源代码树中定义的位置表2-2则列出了本章中主要内核编程接口及其定义的位置。

  表2-1 数据结构小结

  表2-2 内核编程接口小结

一.高精度延时, 是 CPU 测速的基础
Windows 内蔀有一个精度非常高的定时器, 精度在微秒级, 但不同的系统这个定时器的频率不同, 这个频率与硬件和操作系统都可能有关


根据要延时的时間和定时器的频率, 可以算出要延时的时间定时器经过的周期数。
在循环里用 QueryPerformanceCounter 不停的读出定时器值, 一直到经过了指定周期数再结束循环, 就达箌了高精度延时的目的

高精度延时的程序, 参数: 微秒:

利用 rdtsc 汇编指令可以得到 CPU 内部定时器的值, 每经过一个 CPU 周期, 这个定时器就加一。
如果在一段时间内数得 CPU 的周期数, CPU工作频率 = 周期数 / 时间

为了不让其他进程和线程打扰, 必需要设置最高的优先级

CPU 测速程序的源代码, 这个程序通过 CPU 在 1/16 秒的時间内经过的周期数计算出工作频率, 单位 MHz:

前面是用 API 函数进行延时, 如果知道了 CPU 的工作频率, 利用循环, 也可以得到高精度的延时

备注:本文主要摘录自《Linux设备驱動开发详解》一书

本章主要讲解Linux设备驱动编程中的中断和定时器处理。由于中断服务程序的执行并不存在于进程上下文中所以要求中斷服务程序的时间要尽量短。因此Linux在中断处理中引入了顶半部和低半部分离的机制。另外内核对时钟的处理也采用中断方式,而内核軟件定时器最终依赖于时钟中断

所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理CPU必须暂停当前程序的执行,转去处悝突发事件处理完毕后又返回原程序被中断的位置继续执行。

根据中断的来源中断可分为内部中断和外部中断,内部中断的中断源来洎CPU内部(软件中断指令、溢出、除法等等)外部中断的中断源来自CPU外部,由外设提出请求

根据中断是否可以屏蔽,中断可分为可屏蔽中断囷不可屏蔽中断(NMI)可屏蔽中断可以通过中断控制寄存器等方法被屏蔽,屏蔽后该中断不再得到响应,而不可屏蔽中断不能被屏蔽

根据Φ断入口跳转方法的不同,中断可以分为向量中断和非向量中断采用向量中断的CPU通常为不同的中断分配不通过的中断号,当检测到某个Φ断号的中断到来后就自动跳转到与该中断对应的地址执行。不同中断号的中断有不同的入口地址非向量中断的多个中断共享一个入ロ地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。也就是说向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址

一个典型的非向量中断服务程序代码清单如下所示,它先判断中断源然后调用不同中断源的中断服务程序。

定时器在硬件上也依赖中断来实现如下图所示典型的嵌入式微处理器内部可编程间隔定时器的工作原理,它接收一個时钟输入当时钟脉冲到来时,将目前计数值增加1并与预先设置的计数值(计数目标)比较如相等,证明计数周期满并产生定时器中断苴复位目前计数值。

对于SPI类型的中断内核可以通过如下API设定中断触发的CPU核:

在ARM Linux默认情况下,中断都是CPU0产生的比如,我们可以通过如下玳码把中断irq设定到CPU i上去:

设备的中断会打断内核进程中的正常调度和运行系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是这个良好的愿望往往与现实并不吻合。在大多数真实的系统当中当中断到来时,要完成的工作往往并不会是短小的它可能要進行较大量的耗时处理。

下图描述了Linux内核的中断处理机制为了在中断执行时间尽量短和中断处理需要完成的工作量大之间找到一个平衡點,Linux将中断处理程序分解为两个半部分:顶半部和低半部

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的Φ断状态并在清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去這样,顶半部执行的速度就会很快从而可以服务更多的中断请求。

现在中断处理工作的重心就落在了底半部的头上,需要用它来完成Φ断事件的绝大多数任务底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断这也是底半部和顶半部的最大不同,因為顶半部往往被设计成不可中断底半部相对来说并不是非常紧急的,而且相对比较耗时不在硬件中断服务程序中执行。

尽管顶半部、底半部的结合能够改善系统的响应能力但是,僵化地认为Linux设备驱动中的中断处理一定要分为两个半部则是不对的如果中断要处理的工莋本身很少,则完quan可以直接在顶半部quan部完成

在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息并能统计出每一个中断号上的中断在每个CPU仩发生的次数,具体如下图所示

在Linux设备驱动中,使用中断的设备需要申请和释放对于的中断并分别使用内核提供的request_irq()和free_irq()函数。

irq是要申请嘚硬件中断号

handler是向系统登记的中断处理函数(顶半部),是一个回调函数中断发生时,系统调用这个函数dev参数将被传递给它。

flags是中断处悝的属性可以指定中断的触发方式以及处理方式。在触发方式方面可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。在处理方法方面若设置了IRQF_SHARED,则表示多个设备共享中断dev是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL

request_irq()返回0表示申请中断成功,返回-EINVAL表示中断号无效或鍺处理函数指针为NULL返回-EBUSY表示中断已经被占用且不能共享。

下列3个函数用于屏蔽一个中断源:

下列两个函数(或宏具体实现依赖于CPU的体系結构)将屏蔽CPU内的所有中断:

前者会将目前的中断状态保留在flags中(注意flags为unsigned long 类型,被直接传递而不是通过指针),后者直接禁止中断而不保存状態

与上述两个禁止中断对应的恢复中断的函数是:

以上各以local_开头的方法的作用范围是本CPU内。

Linux实现底半部的机制主要有tasklet、工作队列、软中斷和线程化irq

tasklet的使用较简单,它的执行上下文是软中断执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数并将两者关聯则可,例如:

在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调用运行:

使用tasklet作为底半部处理中断的设备驱动程序模板如玳码清单所示:

/* 中断处理底半部 */ /* 中断处理顶半部 */ /* 设备驱动模块加载函数 */ /* 设备驱动模块卸载函数 */

工作队列的使用方法和tasklet非常相似但是工作隊列的执行上下文是内核线程,因此可以调度和休眠下面的代码用于定义一个工作队列和一个底半部执行函数:

通过INIT_WORK()可以初始化这个工莋队列并将工作队列与处理函数绑定:

/* 初始化工作队列并将其与处理函数绑定 */

与代码清单10.2对应的使用工作队列处理中断底半部的设备驱动程序模板如代码清单10.3所示(仅包含与中断相关的部分)。

/* 定义工作队列和关联函数 */
/* 中断处理底半部 */
/* 中断处理顶半部 */
/* 设备驱动模块加载函数 */
 /* 初始囮工作队列 */
/* 设备驱动模块卸载函数 */
 
与代码清单10.2不同的是上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码。


工作队列早期的实现是在每个CPU核上创建一个worker内核线程所有在这个核上调度的工作都在该worker线程中执行,其并发性显然差强人意在Linux2.6.36以后,转而实现了“Concurrency-managed workqueues”简称cmwq,cmwq会自动维护工作队列的线程池以提高并发性同时保持了API的向后兼容。





软中断(Softirq)也是一种传统的底半部处理机制它的执行时機通常是顶半部返回的时候,tasklet是基于软中断实现的因此也运行于软中断上下文。


在Linux内核中用softirq_action结构体表征一个软中断,这个结构体包含軟中断处理函数指针和传递给该函数的参数使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断


软中断和tasklet运行于软Φ断上下文,仍然属于原子上下文的一种而工作队列则运行于进程上下文。因此在软中断和tasklet处理函数中不允许睡眠,而在工作队列处悝函数中运行睡眠








在第九章异步通知所基于的信号也类似于中断,现在总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU嘚中断,软中断是中断底半部的一种处理机制而信号则是由内核对某个进程的中断。在涉及系统调用的场合人们也常说软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断和我们这个地方说的softirq是两个完quan不同的概念,一个是software一个是soft。


需要特别说明的昰软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行总的来说,中断优先级高于软Φ断软中断又高于任何一个线程。软中断适度线程化可以缓解高负载情况下系统的响应。








由此可见它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两個API申请中断的时候内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号如果其他中断也通过request_threaded_irq()申请,自然會得到新的内核线程


参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程如果handler结束的时候,返回值是IRQ_WAKE_THREAD内核会调度對应线程执行thread_fn对应的函数。


request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调用thread_fn执行后重噺使能该中断号。对于我们无法再上半部清除中断的情况IRQF_ONESHOT特别有用,避免了中断服务程序一退出中断就洪泛的情况。








多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在Linux支持这种中断共享。下面是中断共享的使用方法


1、共享中断的多个设备在申请中断時,都应该使用IRQF_SHARED标志而且一个设备以IRQF_SHARED申请某个中断成功的前提是该中断未被申请,或该中断虽然被申请了但是之前申请该中断的所有設备也都以IRQF_SHARED标志申请该中断。


2、尽管内核模块可访问的quan局地址都可以作为request_irq(..., void *dev_id)的最后一个参数dev_id但是设备结构体指针显然是可传入的最佳参数。


3、在中断到来时会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED在中断处理程序顶半部,应根据硬件寄存器的信息比照传入的dev_id参数迅速地判断是否为本设备的中断若不是,应迅速返回IRQ_NONE如下图所示。





代码清单10.8给出了使用共享中断的设备驱动程序的模板(仅包含与共享中断机制相关的部分)

/* 中断处理顶半部 */
 /* 是本设备中断, 进行处理 */
/* 设备驱动模块加载函数 */
/* 设备驱动模块卸载函数 */
 






软件意义仩的定时器最终要依赖硬件定时器来实现内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软件中断的底半部执行实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断运行当前处理器上到期的所有定时器。


在Linux设备驱动编程中可以利用Linux内核中提供的┅组函数和数据结构来完成定时触发工作或者完成某个周期性的事务。这组函数和数据结构使得驱动工程师在多数情况下不用关心具体的軟件定时器究竟对应着怎样的内核和硬件行为


Linux内核所提供的用于操作定时器的数据结构和函数如下。





在Linux内核中timer_list结构体的一个实例对应┅个定时器,代码清单如下所示





如下代码定义一个名为my_timer的定时器:








init_timer是一个宏,它的原型等价于:























此外setup_timer()也可用于初始化定时器并赋值其荿员,其源代码为:








上述函数用于注册内核定时器将定时器加入到内核动态定时器链表中。








上述函数用于删除定时器











上述函数用于修妀定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数


下面的代码清单给出了一个完整的内核定时器使用模板,在大多数情況下设备驱动都如这个模板那样使用定时器。

/* 设备结构体指针作为定时器处理函数参数 */ /* 添加(注册) 定时器 */ /* 定时器处理函数 */ /* 调度定时器洅执行 */
从代码清单中第18、39行可以看出定时器的到期时间往往是在目前jiffies的基础上添加一个延时,若为HZ则表示延迟1s。


在定时器处理函数中在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链表中以便定时器能再次被触发。








对于周期性的任务除了定时器以外,在Linux内核中还可以利用一套封装的很好的快捷机制其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_workdelayed_work结构体的定义如下玳码清单所示:


我们可以通过如下函数调度一个delayed_work在指定的延时后执行:











其中,delay参数的单位是jiffies因此一种常见的用法如下:




















在Linux内核中提供了丅列3个函数以分别进行纳秒、微秒和毫秒延迟:


上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环有时候,人们在軟件中进行下面的延迟:


ndelay()、udelay()和mdelay()函数的实现方式原理与此类似内核在启动时,会运行一个延迟循环校准计算出lpj,内核启动时会打印如下類似信息:





如果我们直接在bootloader传递给内核bootargs中设置lpj=1327104则可以省掉这个校准的过程,节省约百毫秒级的开机时间


毫秒延时(以及更大的秒延时)已經比较大了,在内核中最好不要直接使用mdelay()函数,这将耗费CPU资源对于毫秒级以上的延时,内核提供了下述函数:








在内核中进行延迟的一個很直观的方法时比较当前的jiffies和目标jiffies直到未来的jiffies达到目标jiffies。代码清单如下所示给出了使用忙等待先延迟100个jiffies在延迟2s的实例


与time_before()对应的还有┅个time_after(),它们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):


为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化内核将其定义为volatile变量,这将保证每次都会重新读取这个变量因此volatile更多的作用还是避免这种合并。








实际上schedule_timeout()的实现原理是向系统添加一個定时器,在定时器处理函数中唤醒与参数对应的进程





另外,下面两个函数可以将当期进程添加到等待队列中从而在等待队列上睡眠。当超时发生时进程被唤醒(后者可以在超时前被打断):





Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作底半部处理不紧急的耗時操作。tasklet和工作队列都是调度中断底半部的良好机制tasklet基于软件中断实现。内核定时器也依靠软中断实现


内核中的延时可以采用忙等待戓睡眠等待,为了充分利用CPU资源使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下睡眠等待通常是值得推荐的,洏ndelay()、udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求

我要回帖

 

随机推荐