全部博文(71)
分类: LINUX
2009-11-07 10:01:17
读写锁
另外一种特殊并发规则机制是读写自旋锁。如果临界区的用途是分隔读和写一个共享的数据结构,但不是同时做,这些锁就很自然适合。多个读线程是允许同时进入临界区的。读自旋锁是这样定义的:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
read_lock(&myrwlock);
/*...临界区...*/
read_unlock(&myrwlock);
如果一个写线程进入临界区,其他读写线程就不被允许进入。要使用写自旋锁,你可以这么写:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
write_lock(&myrwlock);
/*...临界区...*/
write_unlock(&myrwlock);
查看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内核引入的序列锁是写多于读的读写锁。这在对一个变量的写操作多于读访问很有用的。例子是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 内核引入了另外一种机制称为Reaad-Copy Update(RCU),在读者多于写者情况下可以提高性能。基本的想法就是读线程可以执行不需锁定。写线程就复杂了,他们在数据结构的拷贝上执行更新操作并替代读者看到的指针。原来的拷贝一直维护到下次所有CPU的上下文切换,以保证所有正在进行中的读操作的完成。RCU数据结构和接口函数定义在include/linux/rcupdate.h。 Documentation/RCU/*有丰富的文档资料。
fs/dcache.c中有RCU的使用例子。linux中每个文件都与目录入口(存储在dentry结构体中)信息、元信息(存储在inode中)和实际数据(存储在数据块中)想关联。当你每次在一个文件上操作时,文件路径中的每部分都被解析并且会得到相应的dentry。为加块未来的操作速度,dentry缓存在名为dcache的数据结构中。任何时候对dcache的查找都会大大多于dcache的更新,所以对dcache的引用要通过RCU保护。
调试
并发相关的麻烦通常上很难去调试因为经常很难重现。在编译和测试代码时使能SMP和抢占是个不错的主意,尽管你的产品级内核即将在单核非抢占下运行。在kernel hacking下面有个配置选项Spinlock and rw-lock debugging(CONFIG_DEBUG_SPINLOCK)可以帮助你捕获一些常见的自旋锁错误。lockmeter()之类的工具可以收集锁相关的统计数据。
通常的并发麻烦都发生在你忘记去锁访问共享资源,这导致不同线程以不可控方式竞争访问它。这个问题称为竞争条件,表现为偶尔的古怪代码行为。
另一种潜在问题是没有释放代码路径中的持有锁,导致死锁。下面代码可让你明白:
spin_lock(&mylock);
/*...临界区...*/
if (error) {
return -EIO;
}
spin_unlock(&mylock);
当错误条件发生后,任何试图得到mylock的线程都会死锁住,内核也可能动弹不了。
如果问题第一次显现是在你写代码的几个月或几年之后,回头去调试它将更加艰难。为避免这样的让你沮丧的相遇,架构软件时就应该设计好并发逻辑。
进程文件系统
进程文件系统(procfs)是一个虚拟文件系统,它创建了进入内核心脏的的“窗户”。你浏览procfs时看到的数据就是内核运行时产生的。procfs中的文件用来配置内核参数、查看内核结构、收集设备驱动统计信息和得到通用系统信息。
procfs是个伪文件系统,意思是驻留在procfs中的文件并不和物理存储设备如硬盘关联。这些文件中的数据随着内核中对应的入口点的需要动态创建。因此procfs中的文件大小显示都为0.procfs通常在内核引导时挂载到/proc。
可以通过查看/proc/cpuinfo /proc/meminfo /proc/interrupts /proc/tty/driver/serial /proc/bus/usb/devices和/proc/stat来得到procfs能力的第一感觉。通过运行时向/proc/sys/中写文件可以改变内核参数。例如:你可以echo一个新的设置到到/proc/sys/kernel/printk来改变内核打印的日志级别。一些工具(如ps)和文件性能监控工具(如sysstat)内部都是从/proc中的文件获取信息。
2.6内核引入的Seq文件简化了大procfs操作。
内存分配
一些设备驱动要意识到内存区的存在。还有一些驱动需要内存分配功能的服务。
内核以页的形式组织物理内存。页大小取决于架构。x86机器上是4k字节。物理内存的每一页都有一个page结构(定义在include/linux/mm_types.h)与之关联:
struct page{
unsigned long flags;
atomic_t _count;
/*...*/
void *virtual;
};
32位x86系统中,缺省内核配置是将4GB的地址空间划分为用户处理的3GB的虚拟内存空间和1GB的内核空间。这暗示了内核可以使用的物理内存有1GB大小限制。事实上限制是896M因为128M地址空间被指派为存储内核数据结构。你可以在内核配置时改变3GB/1GB划分以增加这个限制。
映射到低896M的内核地址和通过常量偏移(称为逻辑地址)表示的物理地址不同。通过高捏成支持,内核通过使用特别的映射产生虚拟地址可以访问896M之外的内存。所有的逻辑地址都是内核虚拟地址,反过来却不一定。
内核存储分区:
1 、 ZONE_DMA(<16MB), 用于DMA,因为遗留ISA设备有24根地址线只可访问头16MB空间。内核会为这些设备独占这片区域。
2、ZONE_NORMAL(16MB~896MB), 正常地址区域,也称为低内存。低内存的struct page里面的virtual域包含对应的逻辑地址。
3、ZONE_HIGH(>896MB),仅仅在映射驻留页到ZONE_NORMAL(通过kmap()和kunmap())才可被内核访问的空间。对应的内核地址是虚拟的不是逻辑的。高内存的struct page中的virtual域为NULL,如果页没有被映射到。
kmalloc()是返回持续的ZONE_NORMAL区域的内存分配函数。原型是:
void *kmalloc(int count, int flags);
count是要分配的字节数,flags是模式符。所有支持的flags列举在include/linux/gfp.h(gfp表示get free pages),下面是通常用到的:
1、GFP_KERNEL进程上下文用来分配内存。如果指定了该标志,kmalloc()可允许进入休眠并等待释放的页。
2、GFP_ATOMIC 中断上下文用来持有内存。这种模式下,kmalloc()不允许睡眠等待空闲页,所以它分配成功可能性是低于GFP_KERNEL的。
由于从kmalloc()返回的内存包含之前存放过的内容,这如果暴露给用户空间就可能是一个安全危险。用kzalloc()可以得到一个kmalloc分配并且零初始化的内存。
如果你需要分配更大的内存缓冲区,而且也不需要物理上连续,就用vmalloc():
void *vmalloc(unsigned long count);
count是请求的分配大小。函数返回内核虚拟地址。
vmalloc()享受比kmalloc()更大的分配大小限制, 但慢而且不能在中断上下文中调用。另外,你不能使用vmalloc()返回的物理非连续的内存执行DMA操作。高性能网络驱动通常在设备打开后使用vmalloc()分配大的描述环。
内核提供了更复杂更专业的内存分配技巧,包括look aside buffers、slabs和mempools。
内核引导相关
内核引导起始于arch/x86/boot/目录下实模式汇编代码的执行。查看arch/x86/kernel/setup_32.c保护模式内核怎么得到实模式内核中收集到的信息。
最开始的引导信息由init/main.c打印的。深入init/calibrate.c可以学习到更多BogoMIPS校准知识,include/asm-you-arch/bugs.h有深入的架构相关的检查。
内核中定时器服务包含在arch/you-arch/kernel/中的架构相关部分和kernel/timer.c中的通用部分实现。相关的定义在头文件include/linux/time*.h中。
jiffies定义在linux/jiffies.h中。处理器相关的HZ值可以在include/asm-you-arch/param.h中找到。