你可以使用mod_timer()修改my_timer的到期时间,使用del_timer()取消定时器,或使用timer_pending()以查看my_timer当前是否处于pending状态。查看kernel/timer.c源代码,你会发现schedule_timeout()内部就使用了这些API。
用户空间的clock_settime()和clock_gettime()函数可用于获得内核定时器服务。用户应用程序可以使用setitimer()和getitimer()来控制一个alarm信号在特定的超时后发生。
短延时
在内核中,小于jiffy的延时被认为是短延时。这种延时在进程或中断上下文都可能发生。由于不可能使用基于jiffy的方法实现短延时,之前讨论的睡眠等待将不再能用于小的超时。这种情况下,唯一的解决途径就是忙等待。
实现短延时的内核API包括mdelay()、udelay()和ndelay(),分别支持毫秒、微妙和纳秒级的延时。这些函数的实际实现依赖于体系结构,而且也并非在所有平台上都被完整实现。忙等待的实现方法是测量CPU执行一条指令的时间,为了延时,执行一定数量的指令。从前文可知,内核会在启动过程中进行测量出一个loops_per_jiffy值。短延时API就使用了loops_per_jiffy值来决定它们需要进行循环的数量。为了实现握手进程中1微妙的延时,USB主机控制器驱动(drivers/usb/host/ehci-hcd.c)会调用udelay(),而udelay()的内部会调用loops_per_jiffy:
do {
result = ehci_readl(ehci, ptr);
/* ... */
if (result == done) return 0;
udelay(1); /* Internally uses loops_per_jiffy */
usec--;
} while (usec > 0);
Pentium时间戳计数器
时间戳计数器(TSC)是Pentium兼容处理器中的一个计数器,它记录自启动以来CPU消耗的时钟周期数。由于TSC睡着处理器周期速率的比例正常,它提供了非常高的精确度。TSC通常被用于剖析和监测代码。使用rdtsc指令可测量某段代码的执行时间,其精度达到微妙级。TSC的节拍可以被转化为秒,方法是将其除以CPU时钟速率(包含在内核变量cpu_khz中)。
在如下的代码片段中。low_tsc_ticks和high_tsc_ticks分别包含了TSC的低32位和高32位。低32位可能在数秒内溢出(具体时间依赖于处理器速度),但是这已经用于许多代码的剖析了:
unsigned long low_tsc_ticks0, high_tsc_ticks0;
unsigned long low_tsc_ticks1, high_tsc_ticks1;
unsigned long exec_time;
rdtsc(low_tsc_ticks0, high_tsc_ticks0); /* Timestamp
before */
printk("Hello World\n"); /* Code to be
profiled */
rdtsc(low_tsc_ticks1, high_tsc_ticks1); /* Timestamp after */
exec_time = low_tsc_ticks1 - low_tsc_ticks0;
在1.8GHz Pentium 处理器上,exec_time的结果为871(或半微妙)。
在2.6.21内核中,针对高精度定时器的支持(CONFIG_HIGH_RES_TIMERS)已经被融入了内核。它使用了硬件特定的高速定时器来提供对nanosleep()等API高精度的支持。在基于Pentium的机器上,内核借助的是TSC。
实时钟
RTC在非易失性存储器上记录绝对时间。在x86 PC上,RTC位于由电池供电[4]的互补金属氧化物半导体(CMOS)存储器的顶部。从第5章《字符设备驱动》的图5.1可以看出传统PC体系结构中CMOS的位置。在嵌入式系统中,RTC可能被集成到处理器中,也可能通过I2C或SPI总线在外部连接,见第8章。
[4]RTC的电池能够持续使用很多年,通过会超过电脑的使用寿命,因此,你从来都不需要替换它。
使用RTC,你可以完成如下工作:
(1)读取、设置绝对时间,在时钟更新时产生中断;
(2)产生频率从2HZ到8192HZ之间的周期性中断;
(3)设置alarm
许多应用程序需要使用绝对时间或称墙上时间(wall time)。jiffies是相对于系统启动后的时间,它不包含墙上时间。内核将墙上时间记录在xtime变量中,在启动过程中,会根据从RTC读取到的目前的墙上时间初始化xtime,在系统停机后,墙上时间会被写回RTC。你可以使用do_gettimeofday()读取墙上时间,其最高精度由硬件决定:
#include
static struct timeval curr_time;
do_gettimeofday(&curr_time);
my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */
用户空间也包含一系列可以访问墙上时间的函数,包括:
(1) time(),该函数返回日历时间,或从Epoch(1970年1月1日00:00:00)以来经历的秒数;
(2) localtime(),以分散的形式返回日历时间;
(3) mktime(), 进行localtime()的反向工作;
(4) gettimeofday(),如果你的平台支持的话,该函数将以微妙精度返回日历时间。
用户空间使用RTC的另一种途径是通过字符设备/dev/rtc来进行,同一时刻只有一个进程允许返回该字符设备。
在第5章和第8章,本书将对RTC驱动进行更深入的讨论。另外,在第19章给出了一个使用/dev/rtc以微妙级精度执行周期性工作的应用程序例子。
内核中的并发
随着多核笔记本电脑时代的到来,对称多处理器(SMP)的使用不再被限于高科技用户。SMP和内核抢占是多线程执行的2种场景。多个线程能够同时操作共享的内核数据结构,因此,对这些数据结构的访问必须被串行化。
接下来,我们会讨论并发访问情况下保护共享内核资源的基本概念。我们以一个简单的例子开始,并逐步引入中断、内核抢占和SMP等复杂概念。
自旋锁和互斥体
访问共享资源的代码区域称作临界区。自选锁(spinlock)和互斥体(mutex,mutual exclusion的缩写)是保护内核临界区的2种基本机制。我们一个一个分析。
自选锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,知道第1个线程释放自选锁。
注意:这里所说的线程不仅限于内核线程,还包含用户线程进入内核后的代表。
下面的例子演示了自选锁的基本用法:
#include
spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */
/* Acquire the spinlock. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, spinlock() has to busy-wait.
*/
spin_lock(&mylock);
/* ... Critical Section code ... */
spin_unlock(&mylock); /* Release the lock */
与自选锁不同的是,互斥体在进入一个被占用的临界区之前,不会原地打转而是使当前线程进入睡眠状态。如果要等待的时间较长,互斥体比自选锁会更合适,因为自选锁会消耗CPU资源。在使用互斥体的场合,多于2次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。
因此,在很多情况下,决定使用自选锁还是互斥体相对来说很容易:
(1)如果临界区需要睡眠,只能使用互斥体,因为在获得自选锁后进行调度、抢占以及在等待队列上睡眠都是非法的;
(2)由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自选锁。(在第4章中,你将学习到更多的关于中断上下文的限制。)
下面的例子演示了互斥体使用的基本方法:
#include
/* Statically declare a mutex. To dynamically
create a mutex, use mutex_init() */
static DEFINE_MUTEX(mymutex);
/* Acquire the mutex. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, mutex_lock() puts the calling thread to sleep.
*/
mutex_lock(&mymutex);
/* ... Critical Section code ... */
mutex_unlock(&mymutex); /* Release the mutex */
为了论证并发保护的用法,我们首先以一个仅存在于进程上下文的临界区开始,并以下面的顺序逐步引入复杂性:
(1)非抢占内核,单CPU情况下存在于进程上下文的临界区;
(2)非抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
(3)可抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
(4)可抢占内核,SMP情况下存在于进程和中断上下文的临界区。
老的信号量接口 互斥体接口代替了老的信号量接口(semaphore),它互斥体诞生于-rt树,在2.6.16内核中被融入主线内核。 尽管如此,但是老的信号量仍然在内核和驱动中被广泛使用。信号量接口的基本用法如下: #include /* Architecture dependent header */ /* Statically declare a semaphore. To dynamically create a semaphore, use init_MUTEX() */ static DECLARE_MUTEX(mysem); down(&mysem); /* Acquire the semaphore */ /* ... Critical Section code ... */ up(&mysem); /* Release the semaphore */ 信号量可以被配置为允许多个预定数量的线程同时进入临界区,但是,这种用法非常罕见。 |
案例1:进程上下文,单CPU,非抢占内核
这种情况最为简单,不需要加锁,因此不再赘述。
案例2:进程和上下文,单CPU,非抢占内核
在这种情况下,为了保护临界区,仅仅需要禁止中断。如图2.4,假定进程上下文的执行单元A、B以及中断上下文的执行单元C都企图进入相同的临界区。
图2.4 进程和中断上下文进入临界区
由于执行单元C总是在中断上下文执行,它会优先于执行单元A和B,因此,它不要担心保护的问题。执行单元A和B也不必关心彼此会被互相打断,因为内核是非抢占的。因此,执行单元A和B仅仅需要担心C会在它们进入临界区的时候横行进入。为了实现此目的,它们会在进入临界区之前禁止中断:
Point A:
local_irq_disable(); /* Disable Interrupts in local CPU */
/* ... Critical Section ... */
local_irq_enable(); /* Enable Interrupts in local CPU */
但是,如果当执行到Point A的时候已经被禁止,local_irq_enable()将产生副作用,它会重新使能中断,而不是恢复之前的中断状态。可以这样修复它:
Point A:
local_irq_save(flags); /* Disable Interrupts */
/* ... Critical Section ... */
local_irq_restore(flags); /* Restore state to what
it was at Point A */
不论Point A的中断处于什么状态,上述工作都将正确执行。
案例3:进程和中断上下文,单CPU,抢占内核
如果内核使能了抢占,仅仅禁止中断将不再能确保对临界区的保护,因为另一个处于进程上下文的执行单元可能会进入临界区。重新回到图2.4,现在,除了C以外,执行单元A和B必须提防彼此。显而易见,解决该问题的方法是在进入临界区之前禁止内核抢占、中断,并在退出临界区的时候恢复内核抢占和中断。因此,执行单元A和B使用了自选锁API附带irq的变体:
unsigned long flags;
Point A:
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore(&mylock, flags);
我们不需要在最后显示地恢复Point A的抢占状态,因为内核自身会通过一个名叫抢占计数器的变量维护它。在抢占被禁止时(通过调用preempt_disable()),计数器会增加;在抢占被使能时(通过调用preempt_enable()),计数器被减少。只有在计数器为0的时候,抢占才发挥作用。
案例4:进程和中断上下文,SMP机器,抢占内核
现在临界区执行于SMP机器上,而且你的内核配置了CONFIG_SMP和CONFIG_PREEMPT。到目前为止讨论的场景中,自旋锁原语发挥的作用仅限于使能和禁止抢占和中断,时间的锁功能并未被完全编译进来。在SMP机器内,锁逻辑被编译进来,而且自旋锁原语确保了SMP安全性。SMP使能的含义如下:
unsigned long flags;
Point A:
/*
- Save interrupt state on the local CPU
- Disable interrupts on the local CPU. This implicitly disables
preemption.
- Lock the section to regulate access by other CPUs
*/
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/*
- Restore interrupt state and preemption to what it
was at Point A for the local CPU
- Release the lock
*/
spin_unlock_irqrestore(&mylock, flags);
在SMP系统上,获取自旋锁时,仅仅本CPU上的中断被禁止。因此,一个进程上下文的执行单元(图2.4中的执行单元A)在一个CPU上运行的同时,一个中断处理函数(图2.4中的执行单元C)可能运行在另一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中断上下文需要调用spin_lock()/spin_unlock():
spin_lock(&mylock);
/* ... Critical Section ... */
spin_unlock(&mylock);
除了有irq变体以外,自旋锁也有底半部(BH)变体。在锁被获取的时候,spin_lock_bh()会禁止底半部,而spin_unlock_bh()则会在锁被释放时重新使能底半部。我们将在第4章讨论底半部。
-rt树 实时(-rt)树,也被称作CONFIG_PREEMPT_RT补丁集,实现了内核中一些针对低延时的修改。该补丁集可以从 |
阅读(1108) | 评论(0) | 转发(0) |