|
发表于 2004-1-25 19:26:59
|
显示全部楼层
Anand K Santhanam IBM
用户界面层
2.6内核重写了帧缓冲/控制台层。这将意味着需要更新各种用户空间帧缓冲工具,如 fbset 和 fbdesl。人机界面层还加入了对近乎所有可接入设备的支持,从触摸屏到盲人用的设备,到各种各样的鼠标。
线程操作可以提高速度;2.6内核现在可以处理任意数目的线程,PID最大可以到20亿(IA32上)。
另外一个变化是引入了 TLS(Thread Local Storage)系统调用,这个调用允许分配一个或多个 GDT(Global Descriptor Table)条目,作为线程注册表。每个 CPU 有一个 GDT,每个条目对应一个线程。这样就可以实现一个不受创建的线程数限制的1:1线程模型(因为每一个新的内核线程都是为一个用户线程而创建)。2.4内核中每个处理器最多只能支持8,192个线程。
系统调用 clone 被扩展,以优化线程的创建。如果 CLONE_PARENT_SETID 标志被设置,内核会把线程ID存储在一个给定的内存位置,如果当线程结束时 CLONE_CLEARID 标志被设置,内核就会把那个内存位置清空。这有助于用户级的内存管理去识别没有使用的内存块。同样,对线程注册表的信号安全加载的支持也已经融入到这个体系中。当 pthread_join 发生时由内核根据线程ID来完成 Futex(fast user space mutex)。(要了解futex的更多信息,请参阅参考资料).
POSIX信号处理在内核空间中完成。一个信号会传递给进程中一个可用的线程;销毁信号会终止整个进程。停止和继续信号也会影响整个进程,这样就可以实现对多线程进程的工作控制。
引入了退出系统调用的一个变种,叫做 exit_group(),这个系统调用终止整个进程和它的线程。此外,退出处理通过引入O(1)算法得到了改进,从而可以在两秒内终止一个具有成千上万个线程的进程(而在2.4内核中完成同样的事情需要15分钟)。
修改了 proc 文件系统,不再报告所有的线程而只是报告原始的线程。这样就避免了 /proc 报告速度的下降。内核保证原始的线程在所有其他线程终止之前不会终止。
虚拟内存的变化
从虚拟内存的角度来看,新内核融合了 Rik van Riel 的 r-map (反向映射,reverse mapping)技术,将显著改善虚拟内存 在一定程度负载下的性能。
为了理解反向映射技术,让我们来首先简单了解 Linux 虚拟内存系统的一些基本原理。
Linux 内核工作于虚拟内存模式:每一个虚拟页对应一个相应的系统内存的物理页。虚拟页和物理页之间的地址转换由硬件的页表来完成。对于一个特定的虚拟页,根据一条页表记录可以找到对应的物理页,或者是页无法找到的提示(说明存在一个页错误)。但是这种"虚拟到物理"的页映射不是总是一一对应的:多个虚拟页(被不同的进程共享的页)有可能指向同一个物理页。在这种情况下,每个共享进程的页记录将有指向对应物理页的映射。如果有类似这样的情况,当内核想要释放特定的物理页时,事情会变得复杂,因为它必须遍历所有的进程页表记录来查找指向这个物理页的引用;它只能在引用数达到0时才能释放这个物理页,因为它没有别的办法可以知道是不是还存在实际指向这个页的引用。这样当负载较高时会让虚拟内存变得非常慢。
反向地址映射补丁通过在结构页引入一个叫做 pte_chain 的数据结构(物理页结构)来解决这一问题。pte_chain 是一个指向页的 PTE 的简单链接列表,可以返回特定的被引用页的 PTE 列表。页释放一下子变得非常简单了。 不过,在这种模式中存在一个指针开销。系统中的每一个结构页都必须有一个额外的用于 pte_chain 的结构。在一个256M内存的系统中,有64K个物理页,这样就需要有 64KB * (sizeof(struct pte_chain)) 的内存被分配用于 pte_chain 的结构――一个很可观的数字。
有一些可以解决这个问题的技术,包括从结构页中删掉 wait_queue_head_t 域(用于对页的独占访问)。因为这个等待队列极少用到,所以在 rmap 补丁中实现了一个更小的队列,通过哈希队列来找到正确的等待队列。
尽管如此,rmap 的性能――尤其是处于高负载的高端系统――相对于2.4内核的虚拟内存系统还是有了显著的提高。
Linux 2.6的驱动程序移植
2.6内核给驱动程序开发人员带来了一系列非常有意义的变化。本节重点介绍将驱动程序从2.4内核移植到2.6内核的一些重要方面。
首先,相对于2.4来说,改进了内核编译系统,从而获得更快的编译速度。加入了改进的图形化工具:make xconfig(需要Qt库)和make gconfig(需要GTK库)。
以下是2.6编译系统的一些亮点:
当使用make时自动创建 arch-zImage 和模块
使用 make -jN 可以进行并行的 make
make 默认的不是冗余方式(可以通过设置 KBUILD_VERBOSE=1 或者使用 make V=1来设置为冗余方式)
make subdir/ 将编译 subdir/ 及其子目录下的所有文件
make help 将提供 make 目标支持
在任何一个阶段都不需要再运行 make dep
内核模块加载器也在2.5中完全被重新实现,这意味着模块编译机制相对于2.4有了很大不同。需要一组新的模块工具来完成模块的加载和缷载 (他们的下载链接可以在参考资料中找到),原来的2.4所用的 makefile 在2.6下不能再用。
新的内核模块加载器是由 Rusty Russel 开发的。它使用内核编译机制,产生一个 .ko(内核目标文件,kernel object)模块目标文件而不是一个 .o 模块目标文件。内核编译系统首先编译这些模块,并将其连接成为 vermagic.o。这一过程在目标模块创建了一个特定部分,以记录使用的编译器版本号,内核版本号,是否使用内核抢占等信息。
现在让我们来看一个例子,分析一下新的内核编译系统如何来编译并加载一个简单的模块。这个模块是一个“hello world”模块,代码和2.4模块代码基本类似,只是 module_init 和 module_exit 要换成 init_module 和 cleanup_module (内核2.4.10模块已经使用这种机制)。这个模块命名为 hello.c,Makefile 文件如下:
清单 3. 驱动程序 makefile 文件示例
KERNEL_SRC = /usr/src/linux
SUBDIR = $(KERNEL_SRC)/drivers/char/hello/
all: modules
obj-m := module.o
hello-objs := hello.o
EXTRA_FLAGS += -DDEBUG=1
modules:
$(MAKE) -C $(KERNEL_SRC) SUBDIR=$(SUBDIR) modules
makefile 文件使用内核编译机制来编译模块。编译好的模块将被命名为 module.ko,并通过编译 hello.c 和连接 vermagic 而获得。KERNEL_SRC 指定内核源文件所在的目录,SUBDIR 指定放置模块的目录。EXTRA_FLAGS 指定了需要给出的编译期标记。
一旦新模块(module.ko)被创建,它可以被新的模块工具加载或缷载。2.4中的原有模块工具不能用来加载或缷载2.6的内核模块。这个新的模块加载工具会尽量减少在一个设备仍在使用的情况下相应的模块却被缷载的冲突发生,而是在确认这些模块已经没有任何设备在使用后再缷载它。产生这种冲突的原因之一是模块使用计数是由模块代码自己来控制的(通过MOD_DEC/INC_USE_COUNT)。
在2.6中,模块不再需要对引用计数进行加或减,这些工作将在模块代码外部进行。任何要引用模块的代码都必须调用 try_module_get(&module),只有在调用成功以后才能访问那个模块;如果被调用的模块已经被缷载,那么这次调用会失败。相应的,可以通过使用 module_put() 来释放对模块的引用。
内存管理的变化
在2.5的开发过程中,加入了内存池,以满足无间断地进行内存分配。其思想是预分配一个内存池,并保留到真正需要的时候。一个内存池由 mempool_create() 调用来创建(应该包含头文件 linux/mempool.h)。
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn, void *pool_data);
在这里 min_nr 是需要预分配对象的数目,alloc_fn 和 free_fn 是指向内存池机制提供的标准对象分配和回收例程的指针。他们的类型是:
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
pool_data 是分配和回收函数用到的指针,gfp_mask 是分配标记。只有当 __GFP_WAIT 标记被指定时,分配函数才会休眠。
在池中分配和回收对象是由以下程序完成的:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
mempool_alloc() 用来分配对象;如果内存池分配器无法提供内存,那么就可以用预分配的池。
系统使用 mempool_destroy() 来回收内存池。
除了为内存分配引入了内存池之外,2.5内核还引入了三个用于常规内存分配的新的GFP标记,它们是:
__GFP_REPEAT -- 告诉页分配器尽力去分配内存。如果内存分配失败过多,应该减少这个标记的使用。
__GFP_NOFAIL -- 不能出现内存分配失败。这样,由于调用者被转入休眠状态,可能需要一段比较长的时间才能完成分配,调用者的需求才能得到满足。
__GFP_NORETRY -- 保证分配失败后不再重试,而向调用者报告失败状态。
除了内存分配的变化以外,remap_page_range()调用——用来映射页到用户空间——也经过了少量修改。相对于2.4来说,现在它多了一个参数。虚拟内存区域(VMA)指针要作为第一个参数,然后是四个常用的参数(start,end,size 和 protection 标记)。
工作队列接口
工作队列接口是在2.5的开发过程中引入的,用于取代任务队列接口(用于调度内核任务)。每个工作队列有一个专门的线程,所有来自运行队列的任务在进程的上下文中运行(这样它们可以休眠)。驱动程序可以创建并使用它们自己的工作队列,或者使用内核的一个工作队列。工作队列用以下方式创建:
struct workqueue_struct *create_workqueue(const char *name);
在这里 name 是工作队列的名字。
工作队列任务可以在编译时或者运行时创建。任务需要封装为一个叫做 work_struct 的结构体。在编译期初始化一个工作队列任务时要用到:
DECLARE_WORK(name, void (*function)(void *), void *data);
在这里 name 是 work_struct 的名字,function 是当任务被调度时调用的函数,data 是指向那个函数的指针。
在运行期初始化一个工作队列时要用到:
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
用下面的函数调用来把一个作业(一个类型为work_struct 结构的工作队列作业/任务)加入到工作队列中:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct
*work, unsigned long delay);
在queue_delay_work()中指定 delay,是为了保证至少在经过一段给定的最小延迟时间以后,工作队列中的任务才可以真正执行。
工作队列中的任务由相关的工作线程执行,可能是在一个无法预期的时间(取决于负载,中断等等),或者是在一段延迟以后。任何一个在工作队列中等待了无限长的时间也没有运行的任务可以用下面的方法取消:
int cancel_delayed_work(struct work_struct *work);
如果当一个取消操作的调用返回时,任务正在执行中,那么这个任务将继续执行下去,但不会再加入到队列中。清空工作队列中的所有任务使用:
void flush_workqueue(struct workqueue_struct *queue);
销毁工作队列使用:
void destroy_workqueue(struct workqueue_struct *queue);
不是所有的驱动程序都必须有自己的工作队列。驱动程序可以使用内核提供的缺省工作队列。由于这个工作队列由很多驱动程序共享,任务可能会需要比较长一段时间才能开始执行。为了解决这一问题,工作函数中的延迟应该保持最小或者干脆不要。
需要特别注意的是缺省队列对所有驱动程序来说都是可用的,但是只有经过GP许可的驱动程序可以用自定义的工作队列:
int schedule_work(struct work_struct *work); -- 向工作队列中添加一个任务
int schedule_delayed_work(struct work_struct *work, unsigned long delay); -- 向工作队列中添加一个任务并延迟执行
当模块被缷载时应该去调用一个 flash_scheduled_work() 函数,这个函数会使等待队列中所有的任务都被执行。
中断例程的变化
2.5的中断处理程序内部已经经历了许多变化,但是绝大部分对于普通的驱动程序开发者来说没有影响。不过,还是有一些重要的变化会影响到驱动程序开发者。
现在的中断处理函数的返回代码是一个 irqreturn_t 类型。这个由 Linus 引入的变化意味着中断处理程序告诉通用的 IRQ 层是否真的要中断。这样做是为了当中断请求不断到来时(原因是驱动程序偶然激活了一个中断位或者硬件坏掉了),捕获假中断(尤其是在共享的PCI线上),而任何驱动程序对此都是无能为力的。在2.6中,驱动程序如果要从一个设备上发出一个中断需要返回 IRQ_HANDLED,如果不是的话返回 IRQ_NONE。这样可以帮助内核的 IRQ 层清楚地识别出哪个驱动程序正在处理那个特定的中断。如果一个中断请求不断到来而且没有注册那个设备的处理程序(例如,所有的驱动程序都返回 IRQ_NONE),内核就会忽略来自那个设备的中断。缺省情况下,驱动程序 IRQ 例程应该返回 IRQ_HANDLED,当驱动程序正在处理那个中断时却返回了 IRQ_NONE,说明存在 bug。新的中断处理程序可能是类似于这样:
清单 4. 2.6的中断处理程序伪代码
irqreturn_t irq_handler(...) {
..
if (!(my_interrupt)
return IRQ_NONE; // not our interrupt
...
return IRQ_HANDLED; // return by default
}
注意,cli(),sti(),save_flags()和 restor_flags() 是不赞成使用的方法。取而代之的是 local_save_flags() 和 local_irq_disable(),用来禁止所有的本地中断(本处理器内的)。禁止所有处理器的中断是不可能的。
统一的设备模型
2.5开发过程中另一个最值得关注的变化是创建了一个统一的设备模型。这个设备模型通过维持大量的数据结构囊括了几乎所有的设备结构和系统。这样做的好处是,可以改进设备的电源管理和简化设备相关的任务管理,包括对以下信息的追踪:
系统中存在的设备,其所连接的总线
特定情形下设备的电源状态
系统清楚设备的驱动程序,并清楚哪些设备受其控制
系统的总线结构:哪个设备连接在哪个总线上,以及哪些总线互连(例如,USB和PCI总线的互连)
设备在系统中的类别描述(类别包括磁盘,分区等等)
在2.5内核中,与设备驱动程序相关的其他发展包括:
不再使用 malloc.h。所有包含 (用于内存分配)的代码现在要替换为 。
用于 x86 体系结构的 HZ 值增加到1000。引入了一个叫做 jiffies_64 的瞬间计算器,以避免由于 HZ 值的变化而引起瞬间变量的迅速溢出。
引入了一个叫做 ndelay() 的新的延迟函数,允许纳秒级的等待。
引入了一个叫做 seqlock() 的新类型的锁,用于锁定小段的经常被访问的数据(不是指针)。
由于2.6内核可以抢占,应该在驱动程序中使用 preempt_disable() 和 preempt_enable(),从而保护代码段不被抢占(禁止 IRQ 同时也就隐式地禁止了抢占)。
在2.5中加入了异步 I/O。这意味着用户进程可以同时进行多个 I/O 操作,而不用等待它们完成。在字符驱动程序中引入了异步 API。
块层在2.5的开发过程中经历了大幅度的变化。这意味着原来用于2.4的块设备需要进行重新设计。
在2.5中引入了sys文件系统,它给出了系统的设备模型的用户空间描述。它挂载在 /sys 目录下。
结束语
由于相对于2.4来说 Linux2.6发生了太多的变化,所以在 Linux 内核界有一种说法是新的发布版本应该命名为3.0。Linus 将最终决定如何命名,官方可能将于2003年11月发布官方版本。不管最终采用哪个版本号,相对于2.4来说,新的内核发布版本在多种平台和体系结构上性能将更快,可扩展性更强,更加稳定。
Linus 已经邀请世界各地的测试人员来查找 bug 和报告问题,并要求发行者提供2.6版本的下载。 |
|