你可以使用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:
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,你可以完成如下工作:
(2)产生频率从2HZ到8192HZ之间的周期性中断;
(3)设置alarm
许多应用程序需要使用绝对时间或称墙上时间(wall time)。jiffies是相对于系统启动后的时间,它不包含墙上时间。内核将墙上时间记录在xtime变量中,在启动过程中,会根据从RTC读取到的目前的墙上时间初始化xtime,在系统停机后,墙上时间会被写回RTC。你可以使用do_gettimeofday()读取墙上时间,其最高精度由硬件决定:
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(),如果你的平台支持的话,该函数将以微妙精度返回日历时间。
内核中的并发
随着多核笔记本电脑时代的到来,对称多处理器(SMP)的使用不再被限于高科技用户。SMP和内核抢占是多线程执行的2种场景。多个线程能够同时操作共享的内核数据结构,因此,对这些数据结构的访问必须被串行化。
接下来,我们会讨论并发访问情况下保护共享内核资源的基本概念。我们以一个简单的例子开始,并逐步引入中断、内核抢占和SMP等复杂概念。
自选锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,知道第1个线程释放自选锁。
注意:这里所说的线程不仅限于内核线程,还包含用户线程进入内核后的代表。
#include <linux/spinlock.h>
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次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。
因此,在很多情况下,决定使用自选锁还是互斥体相对来说很容易:
(2)由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自选锁。(在第4章中,你将学习到更多的关于中断上下文的限制。)
#include <linux/mutex.h>
/* Statically declare a mutex. To dynamically
create a mutex, use mutex_init() */
static DEFINE_MUTEX(mymutex);
/* Acquire the mutex. This is inexpensive if there
* contention, mutex_lock() puts the calling thread to sleep.
mutex_lock(&mymutex);
mutex_unlock(&mymutex); /* Release the mutex */
(1)非抢占内核,单CPU情况下存在于进程上下文的临界区;
(2)非抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
(3)可抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
(4)可抢占内核,SMP情况下存在于进程和中断上下文的临界区。
互斥体接口代替了老的信号量接口(semaphore),它互斥体诞生于-rt树,在2.6.16内核中被融入主线内核。
尽管如此,但是老的信号量仍然在内核和驱动中被广泛使用。信号量接口的基本用法如下:
#include <asm/semaphore.h> /* Architecture dependent
header */
/* Statically declare a semaphore. To dynamically
create a semaphore, use init_MUTEX() */
static DECLARE_MUTEX(mysem);
down(&mysem); /* Acquire the semaphore */
up(&mysem); /* Release the semaphore */
案例1:进程上下文,单CPU,非抢占内核
这种情况最为简单,不需要加锁,因此不再赘述。
案例2:进程和上下文,单CPU,非抢占内核
在这种情况下,为了保护临界区,仅仅需要禁止中断。如图2.4,假定进程上下文的执行单元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;
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave(&mylock, flags);
/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore(&mylock, flags);
我们不需要在最后显示地恢复Point A的抢占状态,因为内核自身会通过一个名叫抢占计数器的变量维护它。在抢占被禁止时(通过调用preempt_disable()),计数器会增加;在抢占被使能时(通过调用preempt_enable()),计数器被减少。只有在计数器为0的时候,抢占才发挥作用。
案例4:进程和中断上下文,SMP机器,抢占内核
/*
- 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
*/
- Restore interrupt state and preemption to what it
was at Point A for the local CPU
- Release the lock
在SMP系统上,获取自旋锁时,仅仅本CPU上的中断被禁止。因此,一个进程上下文的执行单元(图2.4中的执行单元A)在一个CPU上运行的同时,一个中断处理函数(图2.4中的执行单元C)可能运行在另一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中断上下文需要调用spin_lock()/spin_unlock():
/* ... Critical Section ... */
spin_unlock(&mylock);
除了有irq变体以外,自旋锁也有底半部(BH)变体。在锁被获取的时候,spin_lock_bh()会禁止底半部,而spin_unlock_bh()则会在锁被释放时重新使能底半部。我们将在第4章讨论底半部。
-rt树
原子操作
if (!skb->cloned ||
/* Atomically decrement and check if the returned value is zero */
!atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 :
1,&skb_shinfo(skb)->dataref)) {
kfree(skb->head);
}
当skb_release_data()执行的时候,另一个调用skbuff_clone()(也定义于net/core/skbuff.c)的执行单元也许在同步地增加数据引用计数:
/* ... */
/* Atomically bump up the data reference count */
atomic_inc(&(skb_shinfo(skb)->dataref));
内核也支持set_bit()、clear_bit()和test_and_set_bit()操作,它们可用于原子地进行位修改。查看include/asm-your-arch/atomic.h文件可以看出你所在体系结构所支持的原子操作。
另一个特定的并发保护机制是自旋锁的变体读者—写者锁。如果每个执行单元在访问临界区的时候要么是读,要么是写共享的数据结构,但是它们都不会同时进行读和写操作,这种锁是最好的选择。多则读执行单元被允许同时进入临界区。读者自旋锁可以这样定义:
read_lock(&myrwlock); /* Acquire reader lock */
/* ... Critical Region ... */
read_unlock(&myrwlock); /* Release lock */
但是,如果一个写执行单元进入了临界区,其他的读和写都不被允许进入。写者锁的用法如下:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
write_lock(&myrwlock); /* Acquire writer lock */
write_unlock(&myrwlock); /* Release lock */
net/ipx/ipx_route.c中的IPX路由代码是使用读者—写者锁的真实例子。一个称作ipx_routes_lock的读者—写者锁将保护IPX路由表的并发访问。要通过查找路由表实现包转发的执行单元需要请求读者锁。需要添加和删除路由表中入口的执行单元必须获取写者锁。由于通过读路由表的情况比更新路由表的情况多地多,使用读者—写者锁重大地提供了性能。
u64 get_jiffies_64(void) /* Defined in kernel/time.c */
{
unsigned long seq;
u64 ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
fs/dcache.c文件中包含一个RCU的使用例子。在Linux中,每个文件都与一个目录入口信息(dentry结构体)、元数据信息(存放在inode中)和实际的数据(存放在数据块中)关联。每次你操作一个文件的时候,文件路径中的组件会被解析,相应的dentry会被获取。为了加速未来的操作,dentry结构体被缓存在称为dcache的数据结构中。任何时候,对dcache进行查找的数量都远多于dcache的更新操作,因此,对dcache的访问适宜用RCU原语进行保护。
spin_lock(&mylock); /* Acquire lock */
if (error) { /* This error condition occurs rarely */
return -EIO; /* Forgot to release the lock! */
spin_unlock(&mylock); /* Release lock */
proc文件系统
proc文件系统(procfs)是一种虚拟的文件系统,它创建内核内部的视窗。你浏览procfs看到的数据是在内核运行过程中产生的。procfs中文件可被用于配置内核参数、查看内核结构体、从设备驱动中收集统计信息或者获取通用的系统信息。
为了取得procfs能力的第一感觉,请查看/proc/cpuinfo、/proc/meminfo、 /proc/interrupts、/proc/tty/driver/serial、/proc/bus/usb/devices和/proc/stat的内容。通过写/proc/sys/目录中的文件可以在运行时修改某些内核参数。例如,通过向/proc/sys/kernel/printk文件echo一个新的值,你可以改变内核printk 日志的级别。许多实用程序(如ps)和系统性能监视工具(如sysstat)就是通过驻留于/proc中的文件来获取信息的。
2.6内核引入的seq文件简化了大的procfs操作。附录C《Seq文件》对此进行了描述。
内存分配
内核会以分页形式组织物理内存,而页大小则依赖于具体的体系结构。在基于x86的机器上,其大小为4096字节。物理内存中的每一页都有一个与之对应的page结构体(定义在include/linux/mm_types.h文件中):
unsigned long flags; /* Page status */
atomic_t _count; /* Reference count */
void * virtual; /* Explained later on */
};
在32位x86系统上,缺省的内核配置会将4GB的地址空间分成给用户空间的3GB的虚拟内存空间和给内核空间的1GB的空间(如图2.5)。这导致内核能处理的处理内存有1GB的限制。现实情况是,限制为896MB,因为地址空间的128MB已经被内核数据结构占据。通过改变3GB/1GB的分割线你可以增加这个限制,但是由于减少了用户进程虚拟地址空间的大小,对于内存密集型的应用程序,可能会导致一些问题。
因此,存在如下的内存zone:
(1)ZONE_DMA (<16MB),该zone用于直接内存访问(DMA)。由于传统的ISA设备有24条地址线,只能访问开始的16MB,因此,内核将该区域献给了这些设备;
kmalloc()是一个用于从ZONE_NORMAL区域返回连续内存的内存分配函数,其原型如下:
void *kmalloc(int count, int flags);
count是要分配的字节数,flags是一个模式说明符。支持的所有标志列在include/linux./gfp.h文件中(gfp的意思是“get free page”),如下的标志最常用:
(2)GFP_ATOMIC,用于在中断上下问获取内存。在这种模式下,kmalloc()不允许进行睡眠等待以获得空闲页,因此GFP_ATOMIC分配成功的可能性比用GFP_KERNEL低。
由于kmalloc()返回的内存保留了“前世”的内容,因此,如果将它暴露给用户空间,可到会导致安全问题,因此我们可以kzalloc()获得被填充为0的内存。
如果你需要分配大的内存缓冲区,而且也不要求内存在物理上联系,可以用vmalloc()代替kmalloc():
count是要请求分配的内存大小,该函数返回内核虚拟地址。
Vmalloc()允许比kmalloc()更大的分配尺寸,但是它更慢,而且不能从中断上下文调用。另外,你不能用vmalloc()返回的物理上不连续的内存执行DMA。在设备打开时,高性能的网络驱动通常会使用vmalloc()来分配较大的描述符环行缓冲区。
内核还提供了一些更复杂的内存分配技术,包括后备缓冲区(look aside buffer)、slab和mempool,它们超出了本章的范围。
内存启动始于执行arch/x86/boot/目录中的实模式汇编代码。查看arch/x86/kernel/setup_32.c文件可以看出保护模式的内核怎样获取实模式内核收集的信息。
第一条信息来自于init/main.c中的代码,深入挖掘init/calibrate.c可以对BogoMIPS校准理解地更清楚,而include/asm-your-arch/bugs.h则包含体系结构相关的检查。
内存管理源代码存放在顶级mm/目录中。
表2.1给出了本章中主要的数据结构以及其在源代码树中定义的位置。表2.2则列出了本章中主要内核编程接口及其定义的位置。
表2.1 数据结构总结
数据结构
位置
描述
HZ
include/asm-your-arch/param.h
Number of times the system timer ticks in 1 second
loops_per_jiffy
init/main.c
timer_list
include/linux/timer.h
timeval
include/linux/time.h
Timestamp
spinlock_t
include/linux/spinlock_types.h
A busy-locking mechanism to ensure that only a single thread enters a critical section
semaphore
include/asm-your-arch/semaphore.h
mutex
include/linux/mutex.h
rwlock_t
Reader-writer spinlock
page
include/linux/mm_types.h
表2.2 内核编程接口总结
time_after()
time_after_eq()
time_before()
ime_before_eq()
include/linux/jiffies.h
Compares the current value of jiffies with a specified future value
schedule_timeout()
kernel/timer.c
wait_event_timeout()
include/linux/wait.h
DEFINE_TIMER()
Statically defines a timer
init_timer()
Dynamically defines a timer
add_timer()
Schedules the timer for execution after the timeout has elapsed
mod_timer()
Changes timer expiration
timer_pending()
Checks if a timer is pending at the moment
udelay()
include/asm-your-arch/delay.h arch/your-arch/lib/delay.c
Busy-waits for the specified number of microseconds
rdtsc()
include/asm-x86/msr.h
Gets the value of the TSC on Pentium-compatible processors
do_gettimeofday()
kernel/time.c
Obtains wall time
local_irq_disable()
include/asm-your-arch/system.h
Disables interrupts on the local CPU
local_irq_enable()
Enables interrupts on the local CPU
local_irq_save()
Saves interrupt state and disables interrupts
local_irq_restore()
spin_lock()
include/linux/spinlock.h kernel/spinlock.c
Acquires a spinlock.
spin_unlock()
include/linux/spinlock.h
Releases a spinlock
spin_lock_irqsave()
spin_unlock_irqrestore()
Restores interrupt state and preemption and releases the lock
DEFINE_MUTEX()
Statically declares a mutex
Dynamically declares a mutex
mutex_lock()
kernel/mutex.c
Acquires a mutex
mutex_unlock()
Releases a mutex
DECLARE_MUTEX()
Statically declares a semaphore
init_MUTEX()
Dynamically declares a semaphore
up()
arch/your-arch/kernel/semaphore.c
Acquires a semaphore
down()
Releases a semaphore
atomic_inc()
atomic_inc_and_test()
atomic_dec()
atomic_dec_and_test()
clear_bit()
set_bit()
test_bit()
test_and_set_bit()
include/asm-your-arch/atomic.h
Atomic operators to perform lightweight operations
read_lock()
read_unlock()
read_lock_irqsave()
read_lock_irqrestore()
write_lock()
write_unlock()
write_lock_irqsave()
write_lock_irqrestore()
Reader-writer variant of spinlocks
down_read()
up_read()
down_write()
up_write()
kernel/rwsem.c
Reader-writer variant of semaphores
read_seqbegin()
read_seqretry()
write_seqlock()
write_sequnlock()
include/linux/seqlock.h
Seqlock operations
kmalloc()
include/linux/slab.h mm/slab.c
Allocates physically contiguous memory from ZONE_NORMAL
kzalloc()
include/linux/slab.h mm/util.c
Obtains zeroed kmalloced memory
kfree()
mm/slab.c
Releases kmalloced memory
vmalloc()
mm/vmalloc.c
本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120804,如需转载请自行联系原作者