《Essential Linux Device Drivers》第2章(下)

你可以使用 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 可能被集成到处理器中,也可能通过 I 2C SPI 总线在外部连接,见第 8 章。
[4]RTC 的电池能够持续使用很多年,通过会超过电脑的使用寿命,因此,你从来都不需要替换它。
使用 RTC ,你可以完成如下工作:
1 )读取、设置绝对时间,在时钟更新时产生中断;
2 )产生频率从 2HZ 8192HZ 之间的周期性中断;
3 )设置 alarm
许多应用程序需要使用绝对时间或称墙上时间( wall time )。 jiffies 是相对于系统启动后的时间,它不包含墙上时间。内核将墙上时间记录在 xtime 变量中,在启动过程中,会根据从 RTC 读取到的目前的墙上时间初始化 xtime ,在系统停机后,墙上时间会被写回 RTC 。你可以使用 do_gettimeofday() 读取墙上时间,其最高精度 硬件 决定:
#include <linux/time.h>
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 <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次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。
因此,在很多情况下,决定使用自选锁还是互斥体相对来说很容易:
1)如果临界区需要睡眠,只能使用互斥体,因为在获得自选锁后进行调度、抢占以及在等待队列上睡眠都是非法的;
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
 * 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 <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 */
 
/* ... Critical Section code ... */
 
up(&mysem);      /* Release the semaphore */
信号量可以被配置为允许多个预定数量的线程同时进入临界区,但是,这种用法非常罕见。
案例 1 :进程上下文,单 CPU ,非抢占内核
这种情况最为简单,不需要加锁,因此不再赘述。
案例 2 :进程和上下文,单 CPU ,非抢占内核
在这种情况下,为了保护临界区,仅仅需要禁止中断。如图2.4,假定进程上下文的执行单元AB以及中断上下文的执行单元C都企图进入相同的临界区。
2.4 进程和中断上下文进入临界区
由于执行单元C总是在中断上下文执行,它会优先于执行单元AB,因此,它不要担心保护的问题。执行单元AB也不必关心彼此会被互相打断,因为内核是非抢占的。因此,执行单元AB仅仅需要担心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()将产生副作用,它会重新使能中断,而不是恢复之前的中断状态。可以这样修复它:
unsigned long flags;
 
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以外,执行单元AB必须提防彼此。显而易见,解决该问题的方法是在进入临界区之前禁止内核抢占、中断,并在退出临界区的时候恢复内核抢占和中断。因此,执行单元AB使用了自选锁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_SMPCONFIG_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补丁集,实现了内核中一些针对低延时的修改。该补丁集可以从[url]www.kernel.org/pub/linux/kernel/projects/rt[/url]下载,它允许内核的大部分位置可被抢占,但是用自旋锁代替了一些互斥体。它也合并了一些高精度的定时器。数个-rt功能已经被融入了主线内核。在工程的wiki页上,能找到详细的文档,地址为[url]http://rt.wiki.kernel.org/[/url]
    为了提供性能,内核也定义了一些针对特定环境的特定的锁原语。使能实用于你的代码执行场景的互斥机制将使你的代码更高效。下面我们来看一下这些特定的互斥机制。
原子操作
    原子操作原子执行轻量级的、仅执行一次的操作,例如修改计数器、有条件的增1、设置位等。原子操作可以确保操作的串行化,不再需要锁进行并发访问保护。原子操作的具体实现依赖于体系结构。
    为了在释放内核网络缓冲区(称为skbuff)之前检查是否还有余留的数据引用,定义于net/core/skbuff.cskb_release_data()函数将进行如下操作:
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));
/* ... */
原子操作的使用将确保数据引用计数不会被这2个执行单元“蹂躏”。它也消除了使用锁去保护单一整型变量的争论。
内核也支持set_bit()clear_bit()test_and_set_bit()操作,它们可用于原子地进行位修改。查看include/asm-your-arch/atomic.h文件可以看出你所在体系结构所支持的原子操作。
者―
另一个特定的并发保护机制是自旋锁的变体读者―写者锁。如果每个执行单元在访问临界区的时候要么是读,要么是写共享的数据结构,但是它们都不会同时进行读和写操作,这种锁是最好的选择。多则读执行单元被允许同时进入临界区。读者自旋锁可以这样定义:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
 
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 */
/* ... Critical Region ... */
write_unlock(&myrwlock);  /* Release lock */
net/ipx/ipx_route.c 中的IPX路由代码是使用读者―写者锁的真实例子。一个称作ipx_routes_lock的读者―写者锁将保护IPX路由表的并发访问。要通过查找路由表实现包转发的执行单元需要请求读者锁。需要添加和删除路由表中入口的执行单元必须获取写者锁。由于通过读路由表的情况比更新路由表的情况多地多,使用读者―写者锁重大地提供了性能。
和传统的自旋锁一样,读者―写者锁也有相应的irq变体:read_lock_irqsave()read_lock_irqrestore()write_lock_irqsave()write_lock_irqrestore()。这些函数的含义与传统自旋锁相应的变体相似。
2.6 内核引入的顺序锁(seqlock)是一种支持写着多于读者的读者―写者锁。在在一个变量的写操作比读操作多地多的情况下,这种锁非常有用。前文讨论的jiffies_64变量就是使用顺序锁的一个例子。写执行单元不必等待一个已经进入临界区的读者,因此,读执行单元也许会它们进入临界区的操作会失败因此需要重试:
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;
}
    写者会使用write_seqlock()write_sequnlock()保护临界区。
    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原语进行保护。
调试
由于难于重现,并发相关的问题通常非常难调试。在编译和测试你的代码的时候使能SMPCONFIG_SMP)和抢占(CONFIG_PREEMPT)是一种很好的理念,即便你的产品将运行在当CPU、禁止抢占的情况下。在Kernel hacking下有一个称为Spinlock and rw-lock debugging的配置选项CONFIG_DEBUG_SPINLOCK,它能帮助你找到一些常见的自旋锁错误。Lockmeter[url]http://oss.sgi.com/projects/lockmeter/[/url])等工具可用于锁相关的统计信息。
一个常见的并发问题在访问共享资源之前忘记加锁。这会导致一些不同的执行单元杂乱地竞争。这种问题,被称作“竞态”,可能会导致一些其他的行为。
另一个可能的问题是在某些代码路径里你忘记了释放锁,这会导致死锁。为了理解这个问题,让我们分析如下代码:
spin_lock(&mylock);     /* Acquire lock */
 
/* ... Critical Section ... */
 
if (error) {            /* This error condition occurs rarely */
  return -EIO; /* Forgot to release the lock! */
}
 
spin_unlock(&mylock);   /* Release lock */
if (error)被满足后,任何要获取mylock的执行单元都会死锁,内核也可能因此而冻结。
如果在你写完代码的数月或数年以后首次出现了问题,回过头来调试它将变地更为辣手。(在第21章《设备驱动的调试》一章的《Kdump》一节有一个相关的调试例子。)因此,为了避免遭遇这种不快,在你设计软件的结构的时候,就应该考虑并发逻辑。
p roc 文件系统
proc 文件系统(procfs)是一种虚拟的文件系统,它创建内核内部的视窗。你浏览procfs看到的数据是在内核运行过程中产生的。procfs中文件可被用于配置内核参数、查看内核结构体、从设备驱动中收集统计信息或者获取通用的系统信息。
Procfs 是一种虚假的文件系统,这意味着驻留于procfs中的文件并不与物理存储设备如硬盘等关联。相反地,这些文件中的数据由内核中相应的入口点按需动态创建。因此,procfs中的文件大小都显示为0Procfs通常被在启动过程中挂载在/proc目录,通过运行mount命令你可以看出这一点。
为了取得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操作。附录CSeq文件》对此进行了描述。
内存分配
一些设备驱动必须意识到内存zone的存在,另外,许多驱动需要内存分配函数的服务。本节我们将简要地对此二者进行讨论。
内核会以分页形式组织物理内存,而页大小则依赖于具体的体系结构。在基于x86的机器上,其大小为4096字节。物理内存中的每一页都有一个与之对应的page结构体(定义在include/linux/mm_types.h文件中):
struct page {
  unsigned long flags; /* Page status */
  atomic_t _count;     /* Reference count */
  /* ... */
  void * virtual;      /* Explained later on */
};
32x86系统上,缺省的内核配置会将4GB的地址空间分成给用户空间的3GB的虚拟内存空间和给内核空间的1GB的空间(如图2.5)。这导致内核能处理的处理内存有1GB的限制。现实情况是,限制为896MB,因为地址空间的128MB已经被内核数据结构占据。通过改变3GB/1GB的分割线你可以增加这个限制,但是由于减少了用户进程虚拟地址空间的大小,对于内存密集型的应用程序,可能会导致一些问题。
2.5 32 PC 系统上缺省的地址空间分布
    内核中用于映射低于896MB物理内存的地址与物理地址之间存在线性便宜,被称作逻辑地址。在支持“高端内存”的情况下,在通过特定的方式映射这些区域产生对应的虚拟地址后,内核将能访问超过896MB的内存。所有的逻辑地址都是内核虚拟地址,而所有的虚拟地址并非一定是逻辑地址。
    因此,存在如下的内存zone:
    1ZONE_DMA (<16MB),该zone用于直接内存访问(DMA)。由于传统的ISA设备有24条地址线,只能访问开始的16MB,因此,内核将该区域献给了这些设备;
2ZONE_NORMAL (16MB to 896MB),常规地址区域,也被称作低端内存。低端内存页的page结构体的virtual字段包含了对应的逻辑地址;
3ZONE_HIGH (>896MB), 仅仅在通过kmap()映射页为虚拟地址后才能访问。(通过kunmap()可去除映射)。相应的内核地址为虚拟地址而非逻辑地址。如果相应的页未被映射的话,高端内存页的page结构体的virtual字段将指向NULL
kmalloc() 是一个用于从ZONE_NORMAL区域返回连续内存的内存分配函数,其原型如下:
void *kmalloc(int count, int flags);
count 是要分配的字节数,flags是一个模式说明符。支持的所有标志列在include/linux./gfp.h文件中(gfp的意思是“get free page”),如下的标志最常用:
1GFP_KERNEL,被进程上下文用来分配内存。如果指定了该标志,kmalloc()将被允许睡眠以等待其他页被释放;
2GFP_ATOMIC,用于在中断上下问获取内存。在这种模式下,kmalloc()不允许进行睡眠等待以获得空闲页,因此GFP_ATOMIC分配成功的可能性比用GFP_KERNEL低。
由于kmalloc()返回的内存保留了“前世”的内容,因此,如果将它暴露给用户空间,可到会导致安全问题,因此我们可以kzalloc()获得被填充为0的内存。
如果你需要分配大的内存缓冲区,而且也不要求内存在物理上联系,可以用vmalloc()代替kmalloc()
void *vmalloc(unsigned long count);
count 是要请求分配的内存大小,该函数返回内核虚拟地址。
Vmalloc() 允许比kmalloc()更大的分配尺寸,但是它更慢,而且不能从中断上下文调用。另外,你不能用vmalloc()返回的物理上不连续的内存执行DMA。在设备打开时,高性能的网络驱动通常会使用vmalloc()来分配较大的描述符环行缓冲区。
内核还提供了一些更复杂的内存分配技术,包括后备缓冲区(look aside buffer)、slabmempool,它们超出了本章的范围。
查看源代码
内存启动始于执行arch/x86/boot/目录中的实模式汇编代码。查看arch/x86/kernel/setup_32.c文件可以看出保护模式的内核怎样获取实模式内核收集的信息。
第一条信息来自于init/main.c中的代码,深入挖掘init/calibrate.c可以对BogoMIPS校准理解地更清楚,而include/asm-your-arch/bugs.h则包含体系结构相关的检查。
内核中的时间服务由驻留于arch/your-arch/kernel/中的体系结构相关的部分和实现于kernel/timer.c中的通用部分组成。从include/linux/time*.h可以获取相关的定义。
jiffies 定义于linux/jiffies.h文件。HZ的值与处理器相关,可以从include/asm-your-arch/param.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
Number of times the processor executes an internal delay-loop in 1 jiffy
timer_list
include/linux/timer.h
Used to hold the address of a routine that you want to execute at some point in the future
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
A sleep-locking mechanism that allows a predetermined number of users to enter a critical section
mutex
include/linux/mutex.h
The new interface that replaces semaphore
rwlock_t
include/linux/spinlock_types.h
Reader-writer spinlock
page
include/linux/mm_types.h
Kernel's representation of a physical memory page
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
Schedules a process to run after a specified timeout has elapsed
wait_event_timeout()
include/linux/wait.h
Resumes execution if a specified condition becomes true or if a timeout occurs
DEFINE_TIMER()
include/linux/timer.h
Statically defines a timer
init_timer()
kernel/timer.c
Dynamically defines a timer
add_timer()
include/linux/timer.h
Schedules the timer for execution after the timeout has elapsed
mod_timer()
kernel/timer.c
Changes timer expiration
timer_pending()
include/linux/timer.h
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()
include/asm-your-arch/system.h
Enables interrupts on the local CPU
local_irq_save()
include/asm-your-arch/system.h
Saves interrupt state and disables interrupts
local_irq_restore()
include/asm-your-arch/system.h
Restores interrupt state to what it was when the matching local_irq_save() was called
spin_lock()
include/linux/spinlock.h kernel/spinlock.c
Acquires a spinlock.
spin_unlock()
include/linux/spinlock.h
Releases a spinlock
spin_lock_irqsave()
include/linux/spinlock.h kernel/spinlock.c
Saves interrupt state, disables interrupts and preemption on local CPU, and locks their critical section to regulate access by other CPUs
spin_unlock_irqrestore()
include/linux/spinlock.h kernel/spinlock.c
Restores interrupt state and preemption and releases the lock
DEFINE_MUTEX()
include/linux/mutex.h
Statically declares a mutex
mutex_init()
include/linux/mutex.h
Dynamically declares a mutex
mutex_lock()
kernel/mutex.c
Acquires a mutex
mutex_unlock()
kernel/mutex.c
Releases a mutex
DECLARE_MUTEX()
include/asm-your-arch/semaphore.h
Statically declares a semaphore
init_MUTEX()
include/asm-your-arch/semaphore.h
Dynamically declares a semaphore
up()
arch/your-arch/kernel/semaphore.c
Acquires a semaphore
down()
arch/your-arch/kernel/semaphore.c
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()
 
include/linux/spinlock.h kernel/spinlock.c
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
Allocates virtually contiguous memory that is not guaranteed to be physically contiguous.

你可能感兴趣的:(linux,职场,驱动,休闲,Essential)