linux内核分析
http://blog.csdn.net/fjt19900921/article/details/8072816启动 当PC启动时,Intel系列的CPU首先进入的是实模式,并开始执行位于地址0xFFFF0处的代码,也就是ROM-BIOS起始位置的代码。BIOS先进行一系列的系统自检,然后初始化位于地址0的中断向量表。最后B
·
http://blog.csdn.net/fjt19900921/article/details/8072816
中断 Linux系统中有很多不同的硬件设备。你可以同步使用这些设备,也就是说你可以发出一个请求,然后等待一直到设备完成操作以后再进行其他的工作。但这种方法的效率却非常的低,因为操作系统要花费很多的等待时间。一个更为有效的方法是发出请求以后,操作系统继续其他的工作,等设备完成操作以后,给操作系统发送一个中断,操作系统再继续处理和此设备有关的操作。 在将多个设备的中断信号送往CPU的中断插脚之前,系统经常使用中断控制器来综合多个设备的中断。这样即可以节约CPU的中断插脚,也可以提高系统设计的灵活性。中断控制器用来控制系统的中断,它包括屏蔽和状态寄存器。设置屏蔽寄存器的各个位可以允许或屏蔽某一个中断,状态寄存器则用来返回系统中正在使用的中断。 大多数处理器处理中断的过程都相同。当一个设备发出中段请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的转移指令所在的内存区域。这些代码一般在CPU的中断方式下运行。在此方式下,将不会再有中断发生。但有些CPU的中断有自己的优先权,这样,更高优先权的中断则可以发生。这意味着第一级的中断处理程序必须拥有自己的堆栈,以便在处理更高级别的中断前保存CPU的执行状态。当中断处理完毕以后,CPU将恢复到以前的状态,继续执行中断处理前正在执行的指令。 中断处理程序十分简单有效,这样,操作系统就不会花太长的时间屏蔽其他的中断。 [设置Softirq] cpu_raise_softirq是一个轮训,唤醒ksoftirqd_CPU0内核线程, 进行管理 cpu_raise_softirq |__cpu_raise_softirq |wakeup_softirqd |wake_up_process ·cpu_raise_softirq [kernel/softirq.c] ·__cpu_raise_softirq [include/linux/interrupt.h] ·wakeup_softirq [kernel/softirq.c] ·wake_up_process [kernel/sched.c] [执行Softirq] 当内核线程ksoftirqd_CPU0被唤醒, 它会执行队列里的工作。当然ksoftirqd_CPU0也是一个死循环: for (; { if (!softirq_pending(cpu)) schedule(); __set_current_state(TASK_RUNNING); while (softirq_pending(cpu)) { do_softirq(); if (current->;need_resched) schedule } __set_current_state(TASK_INTERRUPTIBLE) } ·ksoftirqd [kernel/softirq.c] [目录] -------------------------------------------------------------------------------- 软中断 发信人: fist (星仔迷), 信区: SysInternals WWW-POST 标 题: 软中断 发信站: 武汉白云黄鹤站 (Thu Mar 22 14:12:46 2001) , 转信 软中断「一」 一、 引言 软中断是linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多cpu 、多线程的软中断处理。要了解软中断,我们必须要先了原来底半处理的处理机制。 二、底半处理机制(基于2.0.3版本) 某些特殊时刻我们并不愿意在核心中执行一些操作。例如中断处理过程中。当中断发生时处理器将停止当前的工作, 操作系统将中断发送到相应的设备驱动上去。由于此时系统中其他程序都不能运行, 所以设备驱动中的中断处理过程不宜过长。有些任务最好稍后执行。Linux底层部分处理机制可以让设备驱动和Linux核心其他部分将这些工作进行排序以延迟执行。 系统中最多可以有32个不同的底层处理过程;bh_base是指向这些过程入口的指针数组。而bh_active和 bh_mask用来表示那些处理过程已经安装以及那些处于活动状态。如果bh_mask的第N位置位则表示bh_base的 第N个元素包含底层部分处理例程。如果bh_active的第N位置位则表示第N个底层处理过程例程可在调度器认 为合适的时刻调用。这些索引被定义成静态的;定时器底层部分处理例程具有最高优先级(索引值为0), 控制台底层部分处理例程其次(索引值为1)。典型的底层部分处理例程包含与之相连的任务链表。例如 immediate底层部分处理例程通过那些需要被立刻执行的任务的立即任务队列(tq_immediate)来执行。 --引自David A Rusling的《linux核心》。 三、对2.4.1 软中断处理机制 下面,我们进入软中断处理部份(softirq.c): 由softirq.c的代码阅读中,我们可以知道,在系统的初始化过程中(softirq_init()),它使用了两个数组:bh_task_vec[32],softirq_vec[32]。其中,bh_task_vec[32]填入了32个bh_action()的入口地址,但soft_vec[32]中,只有softirq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和tasklet_hi_action()的地址。其余的保留它用。 当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的active 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临(如:schedule(), 系统调用返回异常时,硬中断处理结束时等),系统调用do_softirq()来测试active位,再调用被激活的进程在这处过程中,软中断的处理与底半处理有了差别,active 和mask不再对应bh_base[nr], 而是对应softirq_vec[32]。在softirq.c中,我们只涉及了softirq_vec[0]、softirq_vec[3]。这两者分别调用了tasklet_action()和tasklet_hi_action()来进行后续处理。这两个过程比较相似,大致如下: 1 锁cpu的tasklet_vec[cpu]链表,取出链表,将原链表清空,解锁,还给系统。 2 对链表进行逐个处理。 3 有无法处理的,(task_trylock(t)失败,可能有别的进程锁定),插回系统链表。至此,系统完成了一次软中断的处理。 接下来有两个问题: 1 bh_base[]依然存在,但应在何处调用? 2 tasklet_vec[cpu]队列是何时挂上的? 四、再探讨 再次考查softirq.c 的bh_action()部份,发现有两个判断: A:if(!spin_trylock(&global_bh_lock))goto:rescue 指明如果global_bh_lock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tasklet_hi_vec[cpu]队列中。等候中断调度。 B:if(!hardirq_trylock(cpu)) goto tescue unlock 此时有硬中断发生,放入队列推迟执行。若为空闲,现在执行。 由此可见,这部分正是对应底半处理的程序,bh_base[]的延时处理正是底半处理的特点,可以推测,如果没有其它函数往tasklet_hi_vec[cpu]队列挂入,那tasklet_hi_vec[cpu]正完全对应着bh_base[]底半处理 在bh_action()中,把bh_ation()挂入tasklet_hi_vec[cpu]的正是mark_bh(),在整个源码树中查找,发现调用mark_bh()的函数很多,可以理解,软中断产生之时,相关的函数会调用mark_bh(),将bh_action挂上tasklet_hi_vec队列,而bh_action()的作用不过是在发现bh_base[nr]暂时无法处理时重返队列的方法。 由此可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到tasklet_schedule()函数: 157 static inline void tasklet_schedule(struct tasklet_struct *t) 158 { 159 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { 160 int cpu = smp_processor_id(); 161 unsigned long flags; 162 163 local_irq_save(flags); 164 t->;next = tasklet_vec[cpu].list; 165 tasklet_vec[cpu].list = t; /*插入队列。 166 __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); 167 local_irq_restore(flags); 168 } 169 } 正是它为tasklet_vec[cpu]队列的建立立下了汗马功劳,在源码树中,它亦被多个模块调用,来完成它的使命。 至此,我们可以描绘一幅完整的软中断处理图了。 现在,再来考查do_softirq()的softirq_vec[32],在interrupt.h中有如下定义: 56 enum 57 { 58 HI_SOFTIRQ=0, 59 NET_TX_SOFTIRQ, 60 NET_RX_SOFTIRQ, 61 TASKLET_SOFTIRQ 62 }; 这四个变量应都是为softirq_vec[]的下标,那么,do_softirq()也将会处理NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,是否还处理其它中断,这有待探讨。也许,这个do_softirq()有着极大的拓展性,等着我们去开发呢。 主要通过__cpu_raise_softirq来设置 在hi_tasklet(也就是一般用于bh的)的处理里面,在处理完当前的队列后,会将补充的队列重新挂上,然后标记(不管是否补充队列里面有tasklet): local_irq_disable(); t->;next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_enable(); 因此,对mark_bh根本不用设置这个active位。对于一般的tasklet也一样: local_irq_disable(); t->;next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); local_irq_enable(); 其它的设置,可以检索上面的__cpu_raise_softirq bottom half, softirq, tasklet, tqueue [bottom half] bh_base[32] | \/ bh_action(); | \/ bh_task_vec[32]; | mark_bh(), tasklet_hi_schedule() \/ task_hi_action bh_base对应的是32个函数,这些函数在bh_action()中调用 static void bh_action(unsigned long nr) { int cpu = smp_processor_id(); if (!spin_trylock(&global_bh_lock)) goto resched; if (!hardirq_trylock(cpu)) goto resched_unlock; if (bh_base[nr]) bh_base[nr](); hardirq_endlock(cpu); spin_unlock(&global_bh_lock); return; resched_unlock: spin_unlock(&global_bh_lock); resched: mark_bh(nr); } 在软中断初始化时,将bh_action()放到bh_task_vec[32]中,bh_task_vec[32]中元素的类型是tasklet_struct,系统使用mark_bh()或task_hi_schedule()函数将它挂到task_hi_vec[]的对列中,在系统调用do_softirq()时执行。 static inline void mark_bh(int nr) { tasklet_hi_schedule(bh_task_vec+nr); } static inline void tasklet_hi_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); t->;next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_restore(flags); } } [softirq] softirq_vec[32]; struct softirq_action { void (*action)(struct softirq_action *); void *data; }; 软中断对应一个softirq_action的结构,在do_softirq()中调用相应的action()做处理。 软中断初始化时只设置了0,3两项,对应的action是task_hi_action和task_action. 1: task_hi_action /\ | tasklet_hi_vec[NR_CPU] struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned; struct tasklet_head { struct tasklet_struct *list; } __attribute__ ((__aligned__(SMP_CACHE_BYTES))); task_hi_action处理的对象是一个tasklet的队列,每个cpu都有一个对应的tasklet队列, 它在tasklet_hi_schedule中动态添加。 3: task_action /\ | tasklet_vec[NR_CPU] [tasklet] struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; 从上面的分析来看tasklet只是一个调用实体,在do_softirq()中被调用。softirq的组织和结构才是最重要的。 [目录] -------------------------------------------------------------------------------- 硬中断 标题 Linux设备驱动程序的中断 作者 coly (journeyman) 时间 07/02/01 11:24 AM Linux设备驱动程序的中断 Coly V0.1 指定参考书:《Linux设备驱动程序》(第一版) 这里总结一下Linux设备驱动程序中涉及的中断机制。 一、前言 Linux的中断宏观分为两种:软中断和硬中断。声明一下,这里的软和硬的意思是指和软件相关以及和硬件相关,而不是软件实现的中断或硬件实现的中断。软中断就是“信号机制”。软中断不是软件中断。Linux通过信号来产生对进程的各种中断操作,我们现在知道的信号共有31个,其具体内容这里略过,感兴趣读者可参看相关参考文献[1]。 一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。 硬中断就是通常意义上的“中断处理程序”,它是直接处理由硬件发过来的中断信号的。当硬中断收到它应当处理的中断信号以后,就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。 对于软中断,我们不做讨论,那是进程调度里要考虑的事情。由于我们讨论的是设备驱动程序的中断问题,所以焦点集中在硬中断里。我们这里讨论的是硬中断,即和硬件相关的中断。 二、中断产生 要中断,是因为外设需要通知操作系统她那里发生了一些事情,但是中断的功能仅仅是一个设备报警灯,当灯亮的时候中断处理程序只知道有事情发生了,但发生了什么事情还要亲自到设备那里去看才行。也就是说,当中断处理程序得知设备发生了一个中断的时候,它并不知道设备发生了什么事情,只有当它访问了设备上的一些状态寄存器以后,才能知道具体发生了什么,要怎么去处理。 设备通过中断线向中断控制器发送高电平告诉操作系统它产生了一个中断,而操作系统会从中断控制器的状态位知道是哪条中断线上产生了中断。PC机上使用的中断控制器是8259,这种控制器每一个可以管理8条中断线,当两个8259级联的时候共可以控制15条中断线。这里的中断线是实实在在的电路,他们通过硬件接口连接到CPU外的设备控制器上。 三、IRQ 并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线勇有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源。要使用中断线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),我们也常把申请一条中断线成为申请一个IRQ或者是申请一个中断号。 IRQ是非常宝贵的,所以我们建议只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无论对IRQ的使用方式是独占还是共享,申请IRQ的过程都是一样的,分为3步: 1.将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的中断中选一个作为该设备的IRQ。 2.通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。 3.根据中断申请函数的返回值决定怎么做:如果成功了万事大吉,如果没成功则或者重新申请或者放弃申请并返回错误。 申请IRQ的过程,在参考书的配的源代码里有详细的描述,读者可以通过仔细阅读源代码中的short一例对中断号申请由深刻的理解。 四、中断处理程序 Linux中的中断处理程序很有特色,它的一个中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。之所以会有上半部和下半部之分,完全是考虑到中断处理的效率。 上半部的功能是“登记中断”。当一个中断发生时,他就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就没事情了--等待新的中断的到来。这样一来,上半部执行的速度就会很快,他就可以接受更多她负责的设备产生的中断了。上半部之所以要快,是因为它是完全屏蔽中断的,如果她不执行完,其它的中断就不能被及时的处理,只能等到这个中断处理程序执行完毕以后。所以,要尽可能多得对设备产生的中断进行服务和处理,中断处理程序就一定要快。 但是,有些中断事件的处理是比较复杂的,所以中断处理程序必须多花一点时间才能够把事情做完。可怎么样化解在短时间内完成复杂处理的矛盾呢,这时候Linux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的。下半部几乎做了中断处理程序所有的事情,因为上半部只是将下半部排到了他们所负责的设备的中断处理队列中去,然后就什么都不管了。下半部一般所负责的工作是察看设备以获得产生中断的事件信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。如果有些时间下半部不知道怎么去做,他就使用著名的鸵鸟算法来解决问题--说白了就是忽略这个事件。 由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。但是有一点一定要注意,那就是如果一个设备中断处理程序正在运行,无论她是运行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序是不能并行的。 在Linux Kernel 2.0以前,中断分为快中断和慢中断(伪中断我们这里不谈),其中快中断的下半部也是不可中断的,这样可以保证它执行的快一点。但是由于现在硬件水平不断上升,快中断和慢中断的运行速度已经没有什么差别了,所以为了提高中断例程事务处理的效率,从Linux kernel 2.0以后,中断处理程序全部都是慢中断的形式了--他们的下半部是可以被中断的。 但是,在下半部中,你也可以进行中断屏蔽--如果某一段代码不能被中断的话。你可以使用cti、sti或者是save_flag、restore_flag来实现你的想法。至于他们的用法和区别,请参看本文指定参考书中断处理部分。 进一步的细节请读者参看本文指定参考书,这里就不再所说了,详细介绍细节不是我的目的,我的目的是整理概念。 五、置中断标志位 在处理中断的时候,中断控制器会屏蔽掉原先发送中断的那个设备,直到她发送的上一个中断被处理完了为止。因此如果发送中断的那个设备载中断处理期间又发送了一个中断,那么这个中断就被永远的丢失了。 之所以发生这种事情,是因为中断控制器并不能缓冲中断信息,所以当前一个中断没有处理完以前又有新的中断到达,他肯定会丢掉新的中断的。但是这种缺陷可以通过设置主处理器(CPU)上的“置中断标志位”(sti)来解决,因为主处理器具有缓冲中断的功能。如果使用了“置中断标志位”,那么在处理完中断以后使用sti函数就可以使先前被屏蔽的中断得到服务。 六、中断处理程序的不可重入性 上一节中我们提到有时候需要屏蔽中断,可是为什么要将这个中断屏蔽掉呢?这并不是因为技术上实现不了同一中断例程的并行,而是出于管理上的考虑。之所以在中断处理的过程中要屏蔽同一IRQ来的新中断,是因为中断处理程序是不可重入的,所以不能并行执行同一个中断处理程序。在这里我们举一个例子,从这里子例中可以看出如果一个中断处理程序是可以并行的话,那么很有可能会发生驱动程序锁死的情况。当驱动程序锁死的时候,你的操作系统并不一定会崩溃,但是锁死的驱动程序所支持的那个设备是不能再使用了--设备驱动程序死了,设备也就死了。 A是一段代码,B是操作设备寄存器R1的代码,C是操作设备寄存器R2的代码。其中激发PS1的事件会使A1产生一个中断,然后B1去读R1中已有的数据,然后代码C1向R2中写数据。而激发PS2的事件会使A2产生一个中断,然后B2删除R1中的数据,然后C2读去R2中的数据。 如果PS1先产生,且当他执行到A1和B1之间的时候,如果PS2产生了,这是A2会产生一个中断,将PS2中断掉(挂到任务队列的尾部),然后删除了R1的内容。当PS2运行到C2时,由于C1还没有向R2中写数据,所以C2将会在这里被挂起,PS2就睡眠在代码C2上,直到有数据可读的时候被信号唤醒。这是由于PS1中的B2原先要读的R1中的数据被PS2中的B2删除了,所以PS1页会睡眠在B1上,直到有数据可读的时候被信号唤醒。这样一来,唤醒PS1和PS2的事件就永远不会发生了,因此PS1和PS2之间就锁死了。 由于设备驱动程序要和设备的寄存器打交道,所以很难写出可以重入的代码来,因为设备寄存器就是全局变量。因此,最简洁的办法就是禁止同一设备的中断处理程序并行,即设备的中断处理程序是不可重入的。 有一点一定要清楚:在2.0版本以后的Linux kernel中,所有的上半部都是不可中断的(上半部的操作是原子性的);不同设备的下半部可以互相中断,但一个特定的下半部不能被它自己所中断(即同一个下半部不能并)。 由于中断处理程序要求不可重入,所以程序员也不必为编写可重入的代码而头痛了。以我的经验,编写可重入的设备驱动程序是可以的,编写可重入的中断处理程序是非常难得,几乎不可能。 七、避免竞争条件的出现 我们都知道,一旦竞争条件出现了,就有可能会发生死锁的情况,严重时可能会将整个系统锁死。所以一定要避免竞争条件的出现。这里我不多说,大家只要注意一点:绝大多数由于中断产生的竞争条件,都是在带有中断的 内核进程被睡眠造成的。所以在实现中断的时候,一定要相信谨慎的让进程睡眠,必要的时候可以使用cli、sti或者save_flag、restore_flag。具体细节请参看本文指定参考书。 八、实现 如何实现驱动程序的中断例程,是各位读者的事情了。只要你们仔细的阅读short例程的源代码,搞清楚编写驱动程序中断例程的规则,就可以编写自己的中断例程了。只要概念正确, 在正确的规则下编写你的代码,那就是符合道理的东西。我始终强调,概念是第一位的,能编多少代码是很其次的,我们一定要概念正确,才能进行正确的思考。 九、小结 本文介绍了Linux驱动程序中的中断,如果读者已经新痒了的话,那么打开机器开始动手吧! Time for you to leave! 参考文献: 1.Linux网络编程 2.编程之道 3.Linux设备驱动程序 4.Mouse drivers 5.Linux Kernel Hacking Guide 6.Unreliable Guide To Hacking The Linux Kernel [目录] -------------------------------------------------------------------------------- 定时器代码分析 时钟和定时器中断 IRQ 0 [Timer] | \|/ |IRQ0x00_interrupt // wrapper IRQ handler |SAVE_ALL --- |do_IRQ | wrapper routines |handle_IRQ_event --- |handler() ->; timer_interrupt // registered IRQ 0 handler |do_timer_interrupt |do_timer |jiffies++; |update_process_times |if (--counter <= 0) { // if time slice ended then |counter = 0; // reset counter |need_resched = 1; // prepare to reschedule |} |do_softirq |while (need_resched) { // if necessary |schedule // reschedule |handle_softirq |} |RESTORE_ALL ·IRQ0x00_interrupt, SAVE_ALL [include/asm/hw_irq.h] ·do_IRQ, handle_IRQ_event [arch/i386/kernel/irq.c] ·timer_interrupt, do_timer_interrupt [arch/i386/kernel/time.c] ·do_timer, update_process_times [kernel/timer.c] ·do_softirq [kernel/soft_irq.c] ·RESTORE_ALL, while loop [arch/i386/kernel/entry.S] 系统启动核心时,调用start_kernal()继续各方面的初始化,在这之前,各种中断都被禁止,只有在完成必要的初始化后,直到执行完Kmalloc_init()后,才允许中断(init\main.c)。与时钟中断有关的部分初始化如下: 调用trap_init()设置各种trap入口,如system_call、GDT entry、LDT entry、call gate等。其中0~17为各种错误入口,18~47保留。 调用init_IRQ()函数设置核心系统的时钟周期为10ms,即100HZ,它是以后按照轮转法进行CPU调度时所依照的基准时钟周期。每10ms产生的时钟中断信号直接输入到第一块8259A的INT 0(即irq0)。初始化中断矢量表中从0x20起的17个中断矢量,用bad_IRQ#_interrupt函数的地址(#为中断号)填写。 调用sched_init()函数,设置启动第一个进程init_task。设置用于管理bottom_half机制的数据结构bh_base[],规定三类事件的中断处理函数,即时钟TIMER_BH、设备TQUEUE_BH和IMMEDIATE_BH。 调用time_init()函数,首先读取当时的CMOS时间,最后调用setup_x86_irq(0,&irq0)函数,把irq0挂到irq_action[0]队列的后面,并把中断矢量表中第0x20项,即timer中断对应的中断矢量改为IRQ0_interrupt函数的地址,在irq0中,指定时间中断服务程序是timer_interrupt, static struct irqaction irq0 = { timer_interrupt, 0, 0, "timer", NULL, NULL} 结构irqaction的定义如下: struct irqaction { void (*handler)(int, void *, struct pt_regs *); /* 中断服务函数入口 */ unsigned long flags; /* 服务允中与否标记 */ unsigned long mask; const char *name; void *dev_id; struct irqaction *next; }; 其中,若flag==SA_INTERRUPT,则中断矢量改为fast_IRQ#_interrupt,在执行中断服务的过程中不允许出现中断,若为其它标记,则中断矢量为IRQ#_interrupt,在执行中断服务的过程中,允许出现中断。 Irq_action的定义与初始化如下: static void (*interrupt[17])(void) = {IRQ#_interrupt}; static void (*fast_interrupt[16])(void) = {fast_IRQ#_interrupt}; static void (*bad_interrupt[16])(void) = {bad_IRQ#_interrupt};(以上#为中断号) static struct irqaction *irq_action[16] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; irq_action是一个全局数组,每个元素指向一个irq队列,共16个irq队列,时钟中断请求队列在第一个队列,即irq_action[0]。当每个中断请求到来时,都调用setup_x86_irq把该请求挂到相应的队列的后面。 以后,系统每10ms产生一次时钟中断信号,该信号直接输入到第一块8259A的INT 0(即irq0)。CPU根据中断矢量表和中断源,找到中断矢量函数入口IRQ0_interrupt(程序运行过程中允许中断)或者fast_IRQ0_interrupt(程序运行过程中不允许中断)或者bad_IRQ0_interrupt(不执行任何动作,直接返回),这些函数由宏BUILD_TIMER_IRQ(chip, nr, mask)展开定义。 宏BUILD_TIMER_IRQ(chip, nr, mask)的定义如下: #define BUILD_TIMER_IRQ(chip,nr,mask) \ asmlinkage void IRQ_NAME(nr); \ asmlinkage void FAST_IRQ_NAME(nr); \ asmlinkage void BAD_IRQ_NAME(nr); \ __asm__( \ "\n"__ALIGN_STR"\n" \ SYMBOL_NAME_STR(fast_IRQ) #nr "_interrupt:\n\t" \ SYMBOL_NAME_STR(bad_IRQ) #nr "_interrupt:\n\t" \ SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ "pushl $-"#nr"-2\n\t" \ SAVE_ALL \ ENTER_KERNEL \ ACK_##chip(mask,(nr&7)) \ "incl "SYMBOL_NAME_STR(intr_count)"\n\t"\ /* intr_count为进入临界区的同步信号量 */ "movl %esp,%ebx\n\t" \ "pushl %ebx\n\t" \ "pushl $" #nr "\n\t" \ /* 把do_irq函数参数压进堆栈 */ "call "SYMBOL_NAME_STR(do_IRQ)"\n\t" \ "addl $8,%esp\n\t" \ "cli\n\t" \ UNBLK_##chip(mask) \ "decl "SYMBOL_NAME_STR(intr_count)"\n\t" \ "incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \ "jmp ret_from_sys_call\n"; 其中nr为中断请求类型,取值0~15。在irq.c中通过语句BUILD_TIMER_IRQ(first, 0, 0x01)调用该宏,在执行宏的过程中处理时钟中断响应程序do_irq()。 函数do_irq()的第一个参数是中断请求队列序号,时钟中断请求传进来的该参数是0。于是程序根据参数0找到请求队列irq_action[0],逐个处理该队列上handler所指的时钟中断请求的服务函数。由于已经指定时钟中断请求的服务函数是timer_interrupt,在函数timer_interrupt中,立即调用do_timer()函数。 函数do_timer()把jiffies和lost_ticks加1,接着就执行mark_bh(TIMER_BH)函数,把bottom_half中时钟队列对应的位置位,表示该队列处于激活状态。在做完这些动作后,程序从函数do_irq()中返回,继续执行以后的汇编代码。于是,程序在执行语句jmp ret_from_sys_call后,跳到指定的位置处继续执行。 代码段jmp ret_from_sys_call及其相关的代码段如下: ALIGN .globl ret_from_sys_call ret_from_sys_call: cmpl $0,SYMBOL_NAME(intr_count) jne 2f 9: movl SYMBOL_NAME(bh_mask),%eax andl SYMBOL_NAME(bh_active),%eax jne handle_bottom_half #ifdef __SMP__ cmpb $(NO_PROC_ID), SYMBOL_NAME(saved_active_kernel_processor) jne 2f #endif movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are testl $(VM_MASK),%eax # different then jne 1f cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ? je 2f 1: sti orl $(IF_MASK),%eax # these just try to make sure andl $~NT_MASK,%eax # the program doesn't do anything movl %eax,EFLAGS(%esp) # stupid cmpl $0,SYMBOL_NAME(need_resched) jne reschedule #ifdef __SMP__ GET_PROCESSOR_OFFSET(%eax) movl SYMBOL_NAME(current_set)(,%eax), %eax #else movl SYMBOL_NAME(current_set),%eax #endif cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals je 2f movl blocked(%eax),%ecx movl %ecx,%ebx # save blocked in %ebx for signal handling notl %ecx andl signal(%eax),%ecx jne signal_return 2: RESTORE_ALL ALIGN signal_return: movl %esp,%ecx pushl %ecx testl $(VM_MASK),EFLAGS(%ecx) jne v86_signal_return pushl %ebx call SYMBOL_NAME(do_signal) popl %ebx popl %ebx RESTORE_ALL ALIGN v86_signal_return: call SYMBOL_NAME(save_v86_state) movl %eax,%esp pushl %eax pushl %ebx call SYMBOL_NAME(do_signal) popl %ebx popl %ebx RESTORE_ALL handle_bottom_half: incl SYMBOL_NAME(intr_count) call SYMBOL_NAME(do_bottom_half) decl SYMBOL_NAME(intr_count) jmp 9f ALIGN reschedule: pushl $ret_from_sys_call jmp SYMBOL_NAME(schedule) # test 另外,一些与时钟中断及bottom half机制有关的数据结构介绍如下: #define HZ 100 unsigned long volatile jiffies=0; 系统每隔10ms自动把它加1,它是核心系统计时的单位。 enum { TIMER_BH = 0, CONSOLE_BH, TQUEUE_BH, DIGI_BH, SERIAL_BH, RISCOM8_BH, SPECIALIX_BH, BAYCOM_BH, NET_BH, IMMEDIATE_BH, KEYBOARD_BH, CYCLADES_BH, CM206_BH }; 现在只定义了13个bottom half队列,将来可扩充到32个队列。 unsigned long intr_count = 0; 相当于信号量的作用。只有其等于0,才可以do_bottom_half。 int bh_mask_count[32]; 用来计算bottom half队列被屏蔽的次数。只有某队列的bh_mask_count数为0,才能enable该队列。 unsigned long bh_active = 0; bh_active是32位长整数,每一位表示一个bottom half队列,该位置1,表示该队列处于激活状态,随时准备在CPU认为合适的时候执行该队列的服务,置0则相反。 unsigned long bh_mask = 0; bh_mask也是32位长整数,每一位对应一个bottom half队列,该位置1,表示该队列可用,并把处理函数的入口地址赋给bh_base,置0则相反。 void (*bh_base[32])(void); bottom half服务函数入口地址数组。定时器处理函数拥有最高的优先级,它的地址存放在bh_base[0],总是最先执行它所指向的函数。 我们注意到,在IRQ#_interrupt和fast_IRQ#_interrupt中断函数处理返回前,都通过语句jmp ret_from_sys_call,跳到系统调用的返回处(见irq.h),如果bottom half队列不为空,则在那里做类似: if (bh_active & bh_mask) { intr_count = 1; do_bottom_half(); intr_count = 0; }(该判断的汇编代码见Entry.S) 的判断,调用do_bottom_half()函数。 在CPU调度时,通过schedule函数执行上述的判断,再调用do_bottom_half()函数。 总而言之,在下列三种时机: CPU调度时 系统调用返回前 中断处理返回前 都会作判断调用do_bottom_half函数。Do_bottom_half函数依次扫描32个队列,找出需要服务的队列,执行服务后把对应该队列的bh_active的相应位置0。由于bh_active标志中TIMER_BH对应的bit为1,因而系统根据服务函数入口地址数组bh_base找到函数timer_bh()的入口地址,并马上执行该函数,在函数timer_bh中,调用函数run_timer_list()和函数run_old_timers()函数,定时执行服务。 TVECS结构及其实现 有关TVECS结构的一些数据结构定义如下: #define TVN_BITS 6 #define TVR_BITS 8 #define TVN_SIZE (1 << TVN_BITS) #define TVR_SIZE (1 << TVR_BITS) #define TVN_MASK (TVN_SIZE - 1) #define TVR_MASK (TVR_SIZE - 1) #define SLOW_BUT_DEBUGGING_TIMERS 0 struct timer_vec { int index; struct timer_list *vec[TVN_SIZE]; }; struct timer_vec_root { int index; struct timer_list *vec[TVR_SIZE]; }; static struct timer_vec tv5 = { 0 }; static struct timer_vec tv4 = { 0 }; static struct timer_vec tv3 = { 0 }; static struct timer_vec tv2 = { 0 }; static struct timer_vec_root tv1 = { 0 }; static struct timer_vec * const tvecs[] = { (struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5 }; #define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0])) static unsigned long timer_jiffies = 0; TVECS结构是一个元素个数为5的数组,分别指向tv1,tv2,tv3,tv4,tv5的地址。其中,tv1是结构timer_vec_root的变量,它有一个index域和有256个元素的指针数组,该数组的每个元素都是一条类型为timer_list的链表。其余四个元素都是结构timer_vec的变量,它们各有一个index域和64个元素的指针数组,这些数组的每个元素也都是一条链表。 函数internal_add_timer(struct timer_list *timer) 函数代码如下: static inline void internal_add_timer(struct timer_list *timer) { /* * must be cli-ed when calling this */ unsigned long expires = timer->;expires; unsigned long idx = expires - timer_jiffies; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; insert_timer(timer, tv1.vec, i); } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >;>; TVR_BITS) & TVN_MASK; insert_timer(timer, tv2.vec, i); } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >;>; (TVR_BITS + TVN_BITS)) & TVN_MASK; insert_timer(timer, tv3.vec, i); } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >;>; (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; insert_timer(timer, tv4.vec, i); } else if (expires < timer_jiffies) { /* can happen if you add a timer with expires == jiffies, * or you set a timer to go off in the past */ insert_timer(timer, tv1.vec, tv1.index); } else if (idx < 0xffffffffUL) { int i = (expires >;>; (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; insert_timer(timer, tv5.vec, i); } else { /* Can only get here on architectures with 64-bit jiffies */ timer->;next = timer->;prev = timer; } } expires 在调用该函数之前,必须关中。对该函数的说明如下: 取出要加进TVECS的timer的激发时间(expires),算出expires与timer_jiffies的差值idx,用来决定该插到哪个队列中去。 若idx小于2^8,则取expires的第0位到第7位的值I,把timer加到tv1.vec中第I个链表的第一个表项之前。 若idx小于2^14,则取expires的第8位到第13位的值I,把timer加到tv2.vec中第I个链表的第一个表项之前。 若idx小于2^20,则取expires的第14位到第19位的值I,把timer加到tv3.vec中第I个链表的第一个表项之前。 若idx小于2^26,则取expires的第20位到第25位的值I,把timer加到tv4.vec中第I个链表的第一个表项之前。 若expires小于timer_jiffies,即idx小于0,则表明该timer到期,应该把timer放入tv1.vec中tv1.index指定的链表的第一个表项之前。 若idx小于2^32,则取expires的第26位到第32位的值I,把timer加到tv5.vec中第I个链表的第一个表项之前。 若idx大等于2^32,该情况只有在64位的机器上才有可能发生,在这种情况下,不把timer加入TVECS结构。 函数cascade_timers(struct timer_vec *tv) 该函数只是把tv->;index指定的那条链表上的所有timer调用internal_add_timer()函数进行重新调整,这些timer将放入TVECS结构中比原来位置往前移一级,比如说,tv4上的timer将放到tv3上去,tv2上的timer将放到tv1上。这种前移是由run_timer_list函数里调用cascade_timers函数的时机来保证的。然后把该条链表置空,tv->;index加1,若tv->;index等于64,则重新置为0。 函数run_timer_list() 函数代码如下: static inline void run_timer_list(void) { cli(); while ((long)(jiffies - timer_jiffies) >;= 0) { struct timer_list *timer; if (!tv1.index) { int n = 1; do { cascade_timers(tvecs[n]); } while (tvecs[n]->;index == 1 && ++n < NOOF_TVECS); } while ((timer = tv1.vec[tv1.index])) { void (*fn)(unsigned long) = timer->;function; unsigned long data = timer->;data; detach_timer(timer); timer->;next = timer->;prev = NULL; sti(); fn(data); cli(); } ++timer_jiffies; tv1.index = (tv1.index + 1) & TVR_MASK; } sti(); } 对run_timer_list函数的说明如下: 关中。 判断jiffies是否大等于timer_jiffies,若不是,goto 8。 判断tv1.index是否为0(即此时系统已经扫描过整个tv1的256个timer_list链表,又回到的第一个链表处,此时需重整TVECS结构),若是,置n为1;若不是,goto 6。 调用cascade_timers()函数把TVECS[n]中由其index指定的那条链表上的timer放到TVECS[n-1]中来。注意:调用cascade_timers()函数后,index已经加1。 判断TVECS[n]->;index是否为1,即原来为0。如果是(表明TVECS[n]上所有都已经扫描一遍,此时需对其后一级的TVECS[++n]调用cascade_timers()进行重整),把n加1,goto 4。 执行tv1.vec上由tv1->;index指定的那条链表上的所有timer的服务函数,并把该timer从链表中移走。在执行服务函数的过程中,允许中断。 timer_jiffies加1,tv1->;index加1,若tv1->;index等于256,则重新置为0,goto 2。 开中,返回。 Linux提供了两种定时器服务。一种早期的由timer_struct等结构描述,由run_old_times函数处理。另一种“新”的服务由timer_list等结构描述,由add_timer、del_timer、cascade_time和run_timer_list等函数处理。 早期的定时器服务利用如下数据结构: struct timer_struct { unsigned long expires; /*本定时器被唤醒的时刻 */ void (*fn)(void); /* 定时器唤醒后的处理函数 */ } struct timer_struct timer_table[32]; /*最多可同时启用32个定时器 */ unsigned long timer_active; /* 每位对应一定时器,置1表示启用 */ 新的定时器服务依靠链表结构突破了32个的限制,利用如下的数据结构: struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data; /* 用来存放当前进程的PCB块的指针,可作为参数传 void (*function)(unsigned long); 给function */ } 表示上述数据结构的图示如下: 在这里,顺便简单介绍一下旧的timer机制的运作情况。 系统在每次调用函数do_bottom_half时,都会调用一次函数run_old_timers()。 函数run_old_timers() 该函数处理的很简单,只不过依次扫描timer_table中的32个定时器,若扫描到的定时器已经到期,并且已经被激活,则执行该timer的服务函数。 间隔定时器itimer 系统为每个进程提供了三个间隔定时器。当其中任意一个定时器到期时,就会发出一个信号给进程,同时,定时器重新开始运作。三种定时器描述如下: ITIMER_REAL 真实时钟,到期时送出SIGALRM信号。 ITIMER_VIRTUAL 仅在进程运行时的计时,到期时送出SIGVTALRM信号。 ITIMER_PROF 不仅在进程运行时计时,在系统为进程运作而运行时它也计时,与ITIMER_VIRTUAL对比,该定时器通常为那些在用户态和核心态空间运行的应用所花去的时间计时,到期时送出SIGPROF信号。 与itimer有关的数据结构定义如下: struct timespec { long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; struct timeval { int tv_sec; /* seconds */ int tv_usec; /* microseconds */ }; struct itimerspec { struct timespec it_interval; /* timer period */ struct timespec it_value; /* timer expiration */ }; struct itimerval { struct timeval it_interval; /* timer interval */ struct timeval it_value; /* current value */ }; 这三种定时器在task_struct中定义: struct task_struct { …… unsigned long timeout; unsigned long it_real_value,it_prof_value,it_virt_value; unsigned long it_real_incr,it_prof_incr,it_virt_incr; struct timer_list real_timer; …… } 在进程创建时,系统把it_real_fn函数的入口地址赋给real_timer.function。(见sched.h) 我们小组分析了三个系统调用:sys_getitimer,sys_setitimer,sys_alarm。 在这三个系统调用中,需用到以下一些函数: 函数static int _getitimer(int which, struct itimerval *value) 该函数的运行过程大致如下: 根据传进的参数which按三种itimer分别处理: 若是ITIMER_REAL,则设置interval为current进程的it_real_incr,val设置为0;判断current进程的real_timer有否设置并挂入TVECS结构中,若有,设置val为current进程real_timer的expires,并把real_timer重新挂到TVECS结构中,接着把val与当前jiffies作比较,若小等于当前jiffies,则说明该real_timer已经到期,于是重新设置val为当前jiffies的值加1。最后把val减去当前jiffies的值,goto 2。 若是ITIMER_VIRTUAL,则分别设置interval,val的值为current进程的it_virt_incr、it_virt_value,goto 2。 若是ITIMER_PROF,则分别设置interval,val的值为current进程的it_prof_incr、it_prof_value,goto 2。 (2)调用函数jiffiestotv把val,interval的jiffies值转换为timeval,返回0。 函数 int _setitimer(int which, struct itimerval *value, struct itimerval *ovalue) 该函数的运行过程大致如下: 调用函数tvtojiffies把value中的interval和value转换为jiffies i 和 j。 判断指针ovalue是否为空,若空,goto ;若不空,则把由which指定类型的itimer存入ovalue中,若存放不成功,goto 4; 根据which指定的itimer按三种类型分别处理: 若是ITIMER_REAL,则从TVECS结构中取出current进程的real_timer,并重新设置current进程的it_real_value和it_real_incr为j和i。若j等于0,goto 4;若不等于0,则把当前jiffies的值加上定时器剩余时间j,得到触发时间。若i小于j,则表明I已经溢出,应该重新设为ULONG_MAX。最后把current进程的real_timer的expires设为i,把设置过的real_timer重新加入TVECS结构,goto 4。 若是ITIMER_VIRTUAL,则设置current进程的it-_virt_value和it_virt_incr为j和i。 若是ITIMER_PROF,则设置current进程的it-_prof_value和it_prof_incr为j和i。 (4)返回0。 函数verify_area(int type, const void *addr, unsigned long size) 该函数的主要功能是对以addr为始址的,长度为size的一块存储区是否有type类型的操作权利。 函数memcpy_tofs(to, from, n) 该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。 函数memcpy_fromfs(from, to, n) 该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。 函数memset((char*)&set_buffer, 0, sizeof(set_buffer)) 该函数的主要功能是把set_buffer中的内容置为0,在这里,即把it_value和it_interval置为0。 现在,我简单介绍一下这三个系统调用: 系统调用sys_getitimer(int which, struct itimerval *value) 首先,若value为NULL,则返回-EFAULT,说明这是一个bad address。 其次,把which类型的itimer取出放入get_buffer。 再次,若存放成功,再确认对value的写权利。 最后,则把get_buffer中的itimer取出,拷入value。 系统调用sys_setitimer(int which, struct itimerval *value,struct itimerval *ovalue) 首先,判断value是否为NULL,若不是,则确认对value是否有读的权利,并把set_buffer中的数据拷入value;若value为NULL,则把set_buffer中的内容置为0,即把it_value和it_interval置为0。 其次,判断ovalue是否为NULL,若不是,则确认对ovalue是否有写的权利。 再次,调用函数_setitimer设置由which指定类型的itimer。 最后,调用函数memcpy_tofs把get_buffer中的数据拷入ovalue,返回。 系统调用sys_alarm(unsigned int seconds) 该系统调用重新设置进程的real_itimer,若seconds为0,则把原先的alarm定时器删掉。并且设interval为0,故只触发一次,并把旧的real_timer存入oldalarm,并返回oldalarm。 [目录] -------------------------------------------------------------------------------- from aka [目录] -------------------------------------------------------------------------------- 硬件中断 硬件中断 硬件中断概述 中断可以用下面的流程来表示: 中断产生源 -->; 中断向量表 (idt) -->; 中断入口 ( 一般简单处理后调用相应的函数) --->;do_IRQ-->; 后续处理(软中断等工作) 具体地说,处理过程如下: 中断信号由外部设备发送到中断芯片(模块)的引脚 中断芯片将引脚的信号转换成数字信号传给CPU,例如8259主芯片引脚0发送的是0x20 CPU接收中断后,到中断向量表IDT中找中断向量 根据存在中断向量中的数值找到向量入口 由向量入口跳转到一个统一的处理函数do_IRQ 在do_IRQ中可能会标注一些软中断,在执行完do_IRQ后执行这些软中断。 下面一一介绍。 8259芯片 本文主要参考周明德《微型计算机系统原理及应用》和billpan的相关帖子 1.中断产生过程 (1)如果IR引脚上有信号,会使中断请求寄存器(Interrupt Request Register,IRR)相应的位置位,比如图中, IR3, IR4, IR5上有信号,那么IRR的3,4,5为1 (2)如果这些IRR中有一个是允许的,也就是没有被屏蔽,那么就会通过INT向CPU发出中断请求信号。屏蔽是由中断屏蔽寄存器(Interrupt Mask Register,IMR)来控制的,比如图中位3被置1,也就是IRR位3的信号被屏蔽了。在图中,还有4,5的信号没有被屏蔽,所以,会向CPU发出请求信号。 (3)如果CPU处于开中断状态,那么在执行指令的最后一个周期,在INTA上做出回应,并且关中断. (4)8259A收到回应后,将中断服务寄存器(In-Service Register)置位,而将相应的IRR复位: 8259芯片会比较IRR中的中断的优先级,如上图中,由于IMR中位3处于屏蔽状态,所以实际上只是比较IR4,I5,缺省情况下,IR0最高,依次往下,IR7最低(这种优先级可以被设置),所以上图中,ISR被设置为4. (5)在CPU发出下一个INTA信号时,8259将中断号送到数据线上,从而能被CPU接收到,这里有个问题:比如在上图中,8259获得的是数4,但是CPU需要的是中断号(并不为4),从而可以到idt找相应的向量。所以有一个从ISR的信号到中断号的转换。在Linux的设置中,4对应的中断号是0x24. (6)如果8259处于自动结束中断(Automatic End of Interrupt AEOI)状态,那么在刚才那个INTA信号结束前,8259的ISR复位(也就是清0),如果不处于这个状态,那么直到CPU发出EOI指令,它才会使得ISR复位。 2.一些相关专题 (1)从8259 在x86单CPU的机器上采用两个8259芯片,主芯片如上图所示,x86模式规定,从8259将它的INT脚与主8259的IR2相连,这样,如果从8259芯片的引脚IR8-IR15上有中断,那么会在INT上产生信号,主8259在IR2上产生了一个硬件信号,当它如上面的步骤处理后将IR2的中断传送给CPU,收到应答后,会通过CAS通知从8259芯片,从8259芯片将IRQ中断号送到数据线上,从而被CPU接收。 由此,我猜测它产生的所有中断在主8259上优先级为2,不知道对不对。 (2)关于屏蔽 从上面可以看出,屏蔽有两种方法,一种作用于CPU, 通过清除IF标记,使得CPU不去响应8259在INT上的请求。也就是所谓关中断。 另一种方法是,作用于8259,通过给它指令设置IMR,使得相应的IRR不参与ISR(见上面的(4)),被称为禁止(disable),反之,被称为允许(enable). 每次设置IMR只需要对端口0x21(主)或0xA1(从)输出一个字节即可,字节每位对应于IMR每位,例如: outb(cached_21,0x21); 为了统一处理16个中断,Linux用一个16位cached_irq_mask变量来记录这16个中断的屏蔽情况: static unsigned int cached_irq_mask = 0xffff; 为了分别对应于主从芯片的8位IMR,将这16位cached_irq_mask分成两个8位的变量: #define __byte(x,y) (((unsigned char *)&(y))[x]) #define cached_21 (__byte(0,cached_irq_mask)) #define cached_A1 (__byte(1,cached_irq_mask)) 在禁用某个irq的时候,调用下面的函数: void disable_8259A_irq(unsigned int irq){ unsigned int mask = 1 << irq; unsigned long flags; spin_lock_irqsave(&i8259A_lock, flags); cached_irq_mask |= mask; /*-- 对这16位变量设置 */ if (irq & /*-- 看是对主8259设置还是对从芯片设置 */ outb(cached_A1,0xA1); /*-- 对从8259芯片设置 */ else outb(cached_21,0x21); /*-- 对主8259芯片设置 */ spin_unlock_irqrestore(&i8259A_lock, flags); } (3)关于中断号的输出 8259在ISR里保存的只是irq的ID,但是它告诉CPU的是中断向量ID,比如ISR保存时钟中断的ID 0,但是在通知CPU却是中断号0x20.因此需要建立一个映射。在8259芯片产生的IRQ号必须是连续的,也就是如果irq0对应的是中断向量0x20,那么irq1对应的就是0x21,... 在i8259.c/init_8259A()中,进行设置: outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */ outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */ if (auto_eoi) outb_p(0x03, 0x21); /* master does Auto EOI */ else outb_p(0x01, 0x21); /* master expects normal EOI */ outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */ outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */ outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be investigated) */ 这样,在IDT的向量0x20-0x2f可以分别填入相应的中断处理函数的地址了。 i386中断门描述符 段选择符和偏移量决定了中断处理函数的入口地址 在这里段选择符指向内核中唯一的一个代码段描述符的地址__KERNEL_CS(=0x10),而这个描述符定义的段为0到4G: --------------------------------------------------------------------------------- ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* not used */ .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ ... ... --------------------------------------------------------------------------------- 而偏移量就成了绝对的偏移量了,在IDT的描述符中被拆成了两部分,分别放在头和尾。 P标志着这个代码段是否在内存中,本来是i386提供的类似缺页的机制,在Linux中这个已经不用了,都设成1(当然内核代码是永驻内存的,但即使不在内存,推测linux也只会用缺页的标志)。 DPL在这里是0级(特权级) 0D110中,D为1,表明是32位程序(这个细节见i386开发手册).110是中断门的标识,其它101是任务门的标识, 111是陷阱(trap)门标识。 Linux对中断门的设置 于是在Linux中对硬件中断的中断门的设置为: init_IRQ(void) --------------------------------------------------------- for (i = 0; i < NR_IRQS; i++) { int vector = FIRST_EXTERNAL_VECTOR + i; if (vector != SYSCALL_VECTOR) set_intr_gate(vector, interrupt[ i]); } ---------------------------------------------------------- 其中,FIRST_EXTERNAL_VECTOR=0x20,恰好为8259芯片的IR0的中断门(见8259部分),也就是时钟中断的中断门),interrupt[ i]为相应处理函数的入口地址 NR_IRQS=224, =256(IDT的向量总数)-32(CPU保留的中断的个数),在这里设置了所有可设置的向量。 SYSCALL_VECTOR=0x80,在这里意思是避开系统调用这个向量。 而set_intr_gate的定义是这样的: ---------------------------------------------------- void set_intr_gate(unsigned int n, void *addr){ _set_gate(idt_table+n,14,0,addr); } ---------------------------------------------------- 其中,需要解释的是:14是标识指明这个是中断门,注意上面的0D110=01110=14;另外,0指明的是DPL. 中断入口 以8259的16个中断为例: 通过宏BUILD_16_IRQS(0x0), BI(x,y),以及 #define BUILD_IRQ(nr) \ asmlinkage void IRQ_NAME(nr); \ __asm__( \ "\n"__ALIGN_STR"\n" \ SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ "pushl $"#nr"-256\n\t" \ "jmp common_interrupt"; 得到的16个中断处理函数为: IRQ0x00_interrupt: push $0x00 - 256 jump common_interrupt IRQ0x00_interrupt: push $0x01 - 256 jump common_interrupt ... ... IRQ0x0f_interrupt: push $0x0f - 256 jump common_interrupt 这些处理函数简单的把中断号-256(为什么-256,也许是避免和内部中断的中断号有冲突)压到栈中,然后跳到common_interrupt 其中common_interrupt是由宏BUILD_COMMON_IRQ()展开: #define BUILD_COMMON_IRQ() \ asmlinkage void call_do_IRQ(void); \ __asm__( \ "\n" __ALIGN_STR"\n" \ "common_interrupt:\n\t" \ SAVE_ALL \ "pushl $ret_from_intr\n\t" \ SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \ "jmp "SYMBOL_NAME_STR(do_IRQ)); .align 4,0x90common_interrupt: SAVE_ALL展开的保护现场部分 push $ret_from_intrcall do_IRQ: jump do_IRQ; 从上面可以看出,这16个的中断处理函数不过是把中断号-256压入栈中,然后保护现场,最后调用do_IRQ .在common_interrupt中,为了使do_IRQ返回到entry.S的ret_from_intr标号,所以采用的是压入返回点ret_from_intr,用jump来模拟一个从ret_from_intr上面对do_IRQ的一个调用。 和IDT的衔接 为了便于IDT的设置,在数组interrupt中填入所有中断处理函数的地址: void (*interrupt[NR_IRQS])(void) = { IRQ0x00_interrupt, IRQ0x01_interrupt, ... ... } 在中断门的设置中,可以看到是如何利用这个数组的。 硬件中断处理函数do_IRQ do_IRQ的相关对象 在do_IRQ中,一个中断主要由三个对象来完成 其中, irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用). 当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。 irq_desc有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。 irq_desc还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。 例如,硬盘1,硬盘2都产生中断IRQx,在do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(对8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。 hw_irq_controller hw_irq_controller有多种: 1.在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是i8259A_irq_type 2.在多CPU的机器上,采用APIC子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过 APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。 中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种: ioapic_level_irq_type ioapic_edge_irq_type (这里指的是intel的APIC,还有其它公司研制的APIC,我没有研究过) 3. Local APIC自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller: lapic_irq_type struct hw_interrupt_type { const char * typename; unsigned int (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*enable)(unsigned int irq); void (*disable)(unsigned int irq); void (*ack)(unsigned int irq); void (*end)(unsigned int irq); void (*set_affinity)(unsigned int irq, unsigned long mask); }; typedef struct hw_interrupt_type hw_irq_controller; startup 是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽 shutdown 反之,使得芯片不再接收中断 enable 设某个引脚可以接收中断,也就是取消屏蔽 disable 屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生 ack 当CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下 (8259, APIC电平,边沿)的处理都不相同 end 在CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。 irqaction 将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构): ----------------------------------------------------- int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char * devname, void *dev_id) ----------------------------------------------------- 参数说明在源文件里说得非常清楚。 handler是硬件处理函数,在下面的代码中可以看得很清楚: --------------------------------------------- do { status |= action->;flags; action->;handler(irq, action->;dev_id, regs); action = action->;next; } while (action); --------------------------------------------- 第二个参数就是action的dev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。 第三个参数是在entry.S中压入的各个积存器的值。 它的大致流程是: 1.在slab中分配一个irqaction,填上必需的数据 以下在函数setup_irq中。 2.找到它的irq对应的结构irq_desc 3.看它是否想对随机数做贡献 4.看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了) 5.挂到队列最后 6.如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作 7在proc/下面登记 register_irq_proc(irq)(这个我不太明白) 将一个处理函数取下: void free_irq(unsigned int irq, void *dev_id) 首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。 将它从队列里剔除。 如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断: if (!desc->;action) { desc->;status |= IRQ_DISABLED; desc->;handler->;shutdown(irq); } 如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它: #ifdef CONFIG_SMP /* Wait to make sure it's not being used on another CPU */ while (desc->;status & IRQ_INPROGRESS) barrier(); #endif kfree(action); do_IRQ asmlinkage unsigned int do_IRQ(struct pt_regs regs) 1.首先取中断号,并且获取对应的irq_desc: int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */ int cpu = smp_processor_id(); irq_desc_t *desc = irq_desc + irq; 2.对中断芯片(模块)应答: desc->;handler->;ack(irq); 3.修改它的状态(注:这些状态我觉得只有在SMP下才有意义): status = desc->;status & ~(IRQ_REPLAY | IRQ_WAITING); status |= IRQ_PENDING; /* we _want_ to handle it */ IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY。 IRQ_WAITING 探测用,探测时,会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。 IRQ_PENDING , IRQ_INPROGRESS是为了确保: 同一个中断号的处理程序不能重入 不能丢失这个中断号的下一个处理程序 具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INPROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给CPU,那么当内核打算再次运行这个中断号对应的处理程序(链)时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING, 然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。 这些状态的操作不是在什么情况下都必须的,事实上,一个CPU,用8259芯片,无论即使是开中断,也不会发生中断重入的情况,因为在这期间,内核把同一中断屏蔽掉了。 多个CPU比较复杂,因为CPU由Local APIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个CPU都可能产生,它们都会调用时钟中断处理函数。 从I/O APIC传过来的中断,如果是电平触发,也不会,因为在结束发出EOI前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么I/O APIC选择不同的CPU,在这两种情况下,会有重入的可能。 /* * If the IRQ is disabled for whatever reason, we cannot * use the action we have. */ action = NULL; if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) { action = desc->;action; status &= ~IRQ_PENDING; /* we commit to handling */ status |= IRQ_INPROGRESS; /* we are handling it *//*进入执行状态*/ } desc->;status = status; /* * If there is no IRQ handler or it was disabled, exit early. Since we set PENDING, if another processor is handling a different instance of this same irq, the other processor will take care of it. */ if (!action) goto out;/*要么该中断没有处理函数;要么被禁止运行(IRQ_DISABLE);要么有一个实例已经在运行了*/ /* * Edge triggered interrupts need to remember * pending events. * This applies to any hw interrupts that allow a second * instance of the same irq to arrive while we are in do_IRQ * or in the handler. But the code here only handles the _second_ * instance of the irq, not the third or fourth. So it is mostly * useful for irq hardware that does not mask cleanly in an * SMP environment. */ for (; { spin_unlock(&desc->;lock); handle_IRQ_event(irq, ®s, action);/*执行函数链*/ spin_lock(&desc->;lock); if (!(desc->;status & IRQ_PENDING))/*发现期间有中断,就再次执行*/ break; desc->;status &= ~IRQ_PENDING; } desc->;status &= ~IRQ_INPROGRESS;/*退出执行状态*/ out: /* * The ->;end() handler has to deal with interrupts which got * disabled while the handler was running. */ desc->;handler->;end(irq);/*给中断芯片一个结束的操作,一般是允许再次接收中断*/ spin_unlock(&desc->;lock); if (softirq_active(cpu) & softirq_mask(cpu)) do_softirq();/*执行软中断*/ return 1; } [目录] -------------------------------------------------------------------------------- 软中断 软中断softirq softirq简介 提出softirq的机制的目的和老版本的底半部分的目的是一致的,都是将某个中断处理的一部分任务延迟到后面去执行。 Linux内核中一共可以有32个softirq,每个softirq实际上就是指向一个函数。当内核执行softirq(do_softirq),就对这32个softirq进行轮询: (1)是否该softirq被定义了,并且允许被执行? (2)是否激活了(也就是以前有中断要求它执行)? 如果得到肯定的答复,那么就执行这个softirq指向的函数。 值得一提的是,无论有多少个CPU,内核一共只有32个公共的softirq,但是每个CPU可以执行不同的softirq,可以禁止/起用不同的softirq,可以激活不同的softirq,因此,可以说,所有CPU有相同的例程,但是 每个CPU却有自己完全独立的实例。 对(1)的判断是通过考察irq_stat[ cpu ].mask相应的位得到的。这里面的cpu指的是当前指令所在的cpu.在一开始,softirq被定义时,所有的cpu的掩码mask都是一样的。但是在实际运行中,每个cpu上运行的程序可以根据自己的需要调整。 对(2)的判断是通过考察irq_stat[ cpu ].active相应的位得到的. 虽然原则上可以任意定义每个softirq的函数,Linux内核为了进一步加强延迟中断功能,提出了tasklet的机制。tasklet实际上也就是一个函数。在第0个softirq的处理函数tasklet_hi_action中,我们可以看到,当执行这个函数的时候,会依次执行一个链表上所有的tasklet. 我们大致上可以把softirq的机制概括成: 内核依次对32个softirq轮询,如果遇到一个可以执行并且需要的softirq,就执行对应的函数,这些函数有可能又会执行一个函数队列。当执行完这个函数队列后,才会继续询问下一个softirq对应的函数。 挂上一个软中断 void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) { unsigned long flags; int i; spin_lock_irqsave(&softirq_mask_lock, flags); softirq_vec[nr].data = data; softirq_vec[nr].action = action; for (i=0; i<NR_CPUS; i++) softirq_mask(i) |= (1<<nr); spin_unlock_irqrestore(&softirq_mask_lock, flags); } 其中对每个CPU的softirq_mask都标注一下,表明这个softirq被定义了。 tasklet 在这个32个softirq中,有的softirq的函数会依次执行一个队列中的tasklet tasklet其实就是一个函数。它的结构如下: struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; next 用于将tasklet串成一个队列 state 表示一些状态,后面详细讨论 count 用来禁用(count = 1 )或者启用( count = 0 )这个tasklet.因为一旦一个tasklet被挂到队列里,如果没有这个机制,它就一定会被执行。 这个count算是一个事后补救措施,万一挂上了不想执行,就可以把它置1。 func 即为所要执行的函数。 data 由于可能多个tasklet调用公用函数,因此用data可以区分不同tasklet. 如何将一个tasklet挂上 首先要初始化一个tasklet,填上相应的参数 void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->;func = func; t->;data = data; t->;state = 0; atomic_set(&t->;count, 0); } 然后调用schedule函数,注意,下面的函数仅仅是将这个tasklet挂到 TASKLET_SOFTIRQ对应的软中断所执行的tasklet队列上去, 事实上,还有其它的软中断,比如HI_SOFTIRQ,会执行其它的tasklet队列,如果要挂上,那么就要调用tasklet_hi_schedule(). 如果你自己写的softirq执行一个tasklet队列,那么你需要自己写类似下面的函数。 static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); /**/ t->;next = tasklet_vec[cpu].list; /**/ tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); local_irq_restore(flags); } } 这个函数中/**/标注的句子用来挂接上tasklet, __cpu_raise_softirq用来激活TASKLET_SOFTIRQ,这样,下次执行do_softirq就会执行这个TASKLET_SOFTIRQ软中断了 __cpu_raise_softirq定义如下: static inline void __cpu_raise_softirq(int cpu, int nr) { softirq_active(cpu) |= (1<<nr); } tasklet的运行方式 我们以tasklet_action为例,来说明tasklet运行机制。事实上,还有一个函数tasklet_hi_action同样也运行tasklet队列。 首先值得注意的是,我们前面提到过,所有的cpu共用32个softirq,但是同一个softirq在不同的cpu上执行的数据是独立的,基于这个原则,tasklet_vec对每个cpu都有一个,每个cpu都运行自己的tasklet队列。 当执行一个tasklet队列时,内核将这个队列摘下来,以list为队列头,然后从list的下一个开始依次执行。这样做达到什么效果呢?在执行这个队列时,这个队列的结构是静止的,如果在运行期间,有中断产生,并且往这个队列里添加tasklet的话,将填加到tasklet_vec[cpu].list中, 注意这个时候,这个队列里的任何tasklet都不会被执行,被执行的是list接管的队列。 见/*1*//*2/之间的代码。事实上,在一个队列上同时添加和运行也是可行的,没这个简洁。 ----------------------------------------------------------------- static void tasklet_action(struct softirq_action *a) { int cpu = smp_processor_id(); struct tasklet_struct *list; /*1*/ local_irq_disable(); list = tasklet_vec[cpu].list; tasklet_vec[cpu].list = NULL; /*2*/ local_irq_enable(); while (list != NULL) { struct tasklet_struct *t = list; list = list->;next; /*3*/ if (tasklet_trylock(t)) { if (atomic_read(&t->;count) == 0) { clear_bit(TASKLET_STATE_SCHED, &t->;state); t->;func(t->;data); /* * talklet_trylock() uses test_and_set_bit that imply * an mb when it returns zero, thus we need the explicit * mb only here: while closing the critical section. */ #ifdef CONFIG_SMP /*?*/ smp_mb__before_clear_bit(); #endif tasklet_unlock(t); continue; } tasklet_unlock(t); } /*4*/ local_irq_disable(); t->;next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); /*5*/ local_irq_enable(); } } ------------------------------------------------------------- /*3*/看其它cpu是否还有同一个tasklet在执行,如果有的话,就首先将这个tasklet重新放到tasklet_vec[cpu].list指向的预备队列(见/*4*/~/*5*/),而后跳过这个tasklet. 这也就说明了tasklet是不可重入的,以防止两个相同的tasket访问同样的变量而产生竞争条件(race condition) tasklet的状态 在tasklet_struct中有一个属性state,用来表示tasklet的状态: tasklet的状态有3个: 1.当tasklet被挂到队列上,还没有执行的时候,是 TASKLET_STATE_SCHED 2.当tasklet开始要被执行的时候,是 TASKLET_STATE_RUN 其它时候,则没有这两个位的设置 其实还有另一对状态,禁止或允许,tasklet_struct中用count表示,用下面的函数操作 ----------------------------------------------------- static inline void tasklet_disable_nosync(struct tasklet_struct *t) { atomic_inc(&t->;count); } static inline void tasklet_disable(struct tasklet_struct *t) { tasklet_disable_nosync(t); tasklet_unlock_wait(t); } static inline void tasklet_enable(struct tasklet_struct *t) { atomic_dec(&t->;count); } ------------------------------------------------------- 下面来验证1,2这两个状态: 当被挂上队列时: 首先要测试它是否已经被别的cpu挂上了,如果已经在别的cpu挂上了,则不再将它挂上,否则设置状态为TASKLET_STATE_SCHED static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { ... ... } 为什么要这样做?试想,如果一个tasklet已经挂在一队列上,内核将沿着这个队列一个个执行,现在如果又被挂到另一个队列上,那么这个tasklet的指针指向另一个队列,内核就会沿着它走到错误的队列中去了。 tasklet开始执行时: 在tasklet_action中: ------------------------------------------------------------ while (list != NULL) { struct tasklet_struct *t = list; /*0*/ list = list->;next; /*1*/ if (tasklet_trylock(t)) { /*2*/ if (atomic_read(&t->;count) == 0) { /*3*/ clear_bit(TASKLET_STATE_SCHED, &t->;state); t->;func(t->;data); /* * talklet_trylock() uses test_and_set_bit that imply * an mb when it returns zero, thus we need the explicit * mb only here: while closing the critical section. */ #ifdef CONFIG_SMP smp_mb__before_clear_bit(); #endif /*4*/ tasklet_unlock(t); continue; } --------------------------------------------------------------- 1 看是否是别的cpu上这个tasklet已经是 TASKLET_STATE_RUN了,如果是就跳过这个tasklet 2 看这个tasklet是否被允许运行? 3 清除TASKLET_STATE_SCHED,为什么现在清除,它不是还没有从队列上摘下来吗?事实上,它的指针已经不再需要的,它的下一个tasklet已经被list记录了(/*0*/)。这样,如果其它cpu把它挂到其它的队列上去一点影响都没有。 4 清除TASKLET_STATE_RUN标志 1和4确保了在所有cpu上,不可能运行同一个tasklet,这样在一定程度上确保了tasklet对数据操作是安全的,但是不要忘了,多个tasklet可能指向同一个函数,所以仍然会发生竞争条件。 可能会有疑问:假设cpu 1上已经有tasklet 1挂在队列上了,cpu2应该根本挂不上同一个tasklet 1,怎么会有tasklet 1和它发生重入的情况呢? 答案就在/*3*/上,当cpu 1的tasklet 1已经不是TASKLET_STATE_SCHED,而它还在运行,这时cpu2完全有可能挂上同一个tasklet 1,而且使得它试图运行,这时/*1*/的判断就起作用了。 软中断的重入 一般情况下,在硬件中断处理程序后都会试图调用do_softirq执行软中断,但是如果发现现在已经有中断在运行,或者已经有软中断在运行,则 不再运行自己调用的中断。也就是说,软中断是不能进入硬件中断部分的,并且软中断在一个cpu上是不可重入的,或者说是串行化的(serialize) 其目的是避免访问同样的变量导致竞争条件的出现。在开中断的中断处理程序中不允许调用软中断可能是希望这个中断处理程序尽快结束。 这是由do_softirq中的 if (in_interrupt()) return; 保证的. 其中, #define in_interrupt() ({ int __cpu = smp_processor_id(); \ (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); }) 前者local_irq_count(_cpu): 当进入硬件中断处理程序时,handle_IRQ_event中的irq_enter(cpu, irq)会将它加1,表明又进入一个硬件中断 退出则调用irq_exit(cpu, irq) 后者local_bh_count(__cpu) : 当进入软中断处理程序时,do_softirq中的local_bh_disable()会将它加1,表明处于软中断中 local_bh_disable(); 一个例子: 当内核正在执行处理定时器的软中断时,这期间可能会发生多个时钟中断,这些时钟中断的处理程序都试图再次运行处理定时器的软中断,但是由于 已经有个软中断在运行了,于是就放弃返回。 软中断调用时机 最直接的调用: 当硬中断执行完后,迅速调用do_softirq来执行软中断(见下面的代码),这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都成功的,见前面关于重入的帖子。 ---------------------------------- |
中断 Linux系统中有很多不同的硬件设备。你可以同步使用这些设备,也就是说你可以发出一个请求,然后等待一直到设备完成操作以后再进行其他的工作。但这种方法的效率却非常的低,因为操作系统要花费很多的等待时间。一个更为有效的方法是发出请求以后,操作系统继续其他的工作,等设备完成操作以后,给操作系统发送一个中断,操作系统再继续处理和此设备有关的操作。 在将多个设备的中断信号送往CPU的中断插脚之前,系统经常使用中断控制器来综合多个设备的中断。这样即可以节约CPU的中断插脚,也可以提高系统设计的灵活性。中断控制器用来控制系统的中断,它包括屏蔽和状态寄存器。设置屏蔽寄存器的各个位可以允许或屏蔽某一个中断,状态寄存器则用来返回系统中正在使用的中断。 大多数处理器处理中断的过程都相同。当一个设备发出中段请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的转移指令所在的内存区域。这些代码一般在CPU的中断方式下运行。在此方式下,将不会再有中断发生。但有些CPU的中断有自己的优先权,这样,更高优先权的中断则可以发生。这意味着第一级的中断处理程序必须拥有自己的堆栈,以便在处理更高级别的中断前保存CPU的执行状态。当中断处理完毕以后,CPU将恢复到以前的状态,继续执行中断处理前正在执行的指令。 中断处理程序十分简单有效,这样,操作系统就不会花太长的时间屏蔽其他的中断。 [设置Softirq] cpu_raise_softirq是一个轮训,唤醒ksoftirqd_CPU0内核线程, 进行管理 cpu_raise_softirq |__cpu_raise_softirq |wakeup_softirqd |wake_up_process ·cpu_raise_softirq [kernel/softirq.c] ·__cpu_raise_softirq [include/linux/interrupt.h] ·wakeup_softirq [kernel/softirq.c] ·wake_up_process [kernel/sched.c] [执行Softirq] 当内核线程ksoftirqd_CPU0被唤醒, 它会执行队列里的工作。当然ksoftirqd_CPU0也是一个死循环: for (; { if (!softirq_pending(cpu)) schedule(); __set_current_state(TASK_RUNNING); while (softirq_pending(cpu)) { do_softirq(); if (current->;need_resched) schedule } __set_current_state(TASK_INTERRUPTIBLE) } ·ksoftirqd [kernel/softirq.c] [目录] -------------------------------------------------------------------------------- 软中断 发信人: fist (星仔迷), 信区: SysInternals WWW-POST 标 题: 软中断 发信站: 武汉白云黄鹤站 (Thu Mar 22 14:12:46 2001) , 转信 软中断「一」 一、 引言 软中断是linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多cpu 、多线程的软中断处理。要了解软中断,我们必须要先了原来底半处理的处理机制。 二、底半处理机制(基于2.0.3版本) 某些特殊时刻我们并不愿意在核心中执行一些操作。例如中断处理过程中。当中断发生时处理器将停止当前的工作, 操作系统将中断发送到相应的设备驱动上去。由于此时系统中其他程序都不能运行, 所以设备驱动中的中断处理过程不宜过长。有些任务最好稍后执行。Linux底层部分处理机制可以让设备驱动和Linux核心其他部分将这些工作进行排序以延迟执行。 系统中最多可以有32个不同的底层处理过程;bh_base是指向这些过程入口的指针数组。而bh_active和 bh_mask用来表示那些处理过程已经安装以及那些处于活动状态。如果bh_mask的第N位置位则表示bh_base的 第N个元素包含底层部分处理例程。如果bh_active的第N位置位则表示第N个底层处理过程例程可在调度器认 为合适的时刻调用。这些索引被定义成静态的;定时器底层部分处理例程具有最高优先级(索引值为0), 控制台底层部分处理例程其次(索引值为1)。典型的底层部分处理例程包含与之相连的任务链表。例如 immediate底层部分处理例程通过那些需要被立刻执行的任务的立即任务队列(tq_immediate)来执行。 --引自David A Rusling的《linux核心》。 三、对2.4.1 软中断处理机制 下面,我们进入软中断处理部份(softirq.c): 由softirq.c的代码阅读中,我们可以知道,在系统的初始化过程中(softirq_init()),它使用了两个数组:bh_task_vec[32],softirq_vec[32]。其中,bh_task_vec[32]填入了32个bh_action()的入口地址,但soft_vec[32]中,只有softirq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和tasklet_hi_action()的地址。其余的保留它用。 当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的active 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临(如:schedule(), 系统调用返回异常时,硬中断处理结束时等),系统调用do_softirq()来测试active位,再调用被激活的进程在这处过程中,软中断的处理与底半处理有了差别,active 和mask不再对应bh_base[nr], 而是对应softirq_vec[32]。在softirq.c中,我们只涉及了softirq_vec[0]、softirq_vec[3]。这两者分别调用了tasklet_action()和tasklet_hi_action()来进行后续处理。这两个过程比较相似,大致如下: 1 锁cpu的tasklet_vec[cpu]链表,取出链表,将原链表清空,解锁,还给系统。 2 对链表进行逐个处理。 3 有无法处理的,(task_trylock(t)失败,可能有别的进程锁定),插回系统链表。至此,系统完成了一次软中断的处理。 接下来有两个问题: 1 bh_base[]依然存在,但应在何处调用? 2 tasklet_vec[cpu]队列是何时挂上的? 四、再探讨 再次考查softirq.c 的bh_action()部份,发现有两个判断: A:if(!spin_trylock(&global_bh_lock))goto:rescue 指明如果global_bh_lock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tasklet_hi_vec[cpu]队列中。等候中断调度。 B:if(!hardirq_trylock(cpu)) goto tescue unlock 此时有硬中断发生,放入队列推迟执行。若为空闲,现在执行。 由此可见,这部分正是对应底半处理的程序,bh_base[]的延时处理正是底半处理的特点,可以推测,如果没有其它函数往tasklet_hi_vec[cpu]队列挂入,那tasklet_hi_vec[cpu]正完全对应着bh_base[]底半处理 在bh_action()中,把bh_ation()挂入tasklet_hi_vec[cpu]的正是mark_bh(),在整个源码树中查找,发现调用mark_bh()的函数很多,可以理解,软中断产生之时,相关的函数会调用mark_bh(),将bh_action挂上tasklet_hi_vec队列,而bh_action()的作用不过是在发现bh_base[nr]暂时无法处理时重返队列的方法。 由此可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到tasklet_schedule()函数: 157 static inline void tasklet_schedule(struct tasklet_struct *t) 158 { 159 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { 160 int cpu = smp_processor_id(); 161 unsigned long flags; 162 163 local_irq_save(flags); 164 t->;next = tasklet_vec[cpu].list; 165 tasklet_vec[cpu].list = t; /*插入队列。 166 __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); 167 local_irq_restore(flags); 168 } 169 } 正是它为tasklet_vec[cpu]队列的建立立下了汗马功劳,在源码树中,它亦被多个模块调用,来完成它的使命。 至此,我们可以描绘一幅完整的软中断处理图了。 现在,再来考查do_softirq()的softirq_vec[32],在interrupt.h中有如下定义: 56 enum 57 { 58 HI_SOFTIRQ=0, 59 NET_TX_SOFTIRQ, 60 NET_RX_SOFTIRQ, 61 TASKLET_SOFTIRQ 62 }; 这四个变量应都是为softirq_vec[]的下标,那么,do_softirq()也将会处理NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,是否还处理其它中断,这有待探讨。也许,这个do_softirq()有着极大的拓展性,等着我们去开发呢。 主要通过__cpu_raise_softirq来设置 在hi_tasklet(也就是一般用于bh的)的处理里面,在处理完当前的队列后,会将补充的队列重新挂上,然后标记(不管是否补充队列里面有tasklet): local_irq_disable(); t->;next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_enable(); 因此,对mark_bh根本不用设置这个active位。对于一般的tasklet也一样: local_irq_disable(); t->;next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); local_irq_enable(); 其它的设置,可以检索上面的__cpu_raise_softirq bottom half, softirq, tasklet, tqueue [bottom half] bh_base[32] | \/ bh_action(); | \/ bh_task_vec[32]; | mark_bh(), tasklet_hi_schedule() \/ task_hi_action bh_base对应的是32个函数,这些函数在bh_action()中调用 static void bh_action(unsigned long nr) { int cpu = smp_processor_id(); if (!spin_trylock(&global_bh_lock)) goto resched; if (!hardirq_trylock(cpu)) goto resched_unlock; if (bh_base[nr]) bh_base[nr](); hardirq_endlock(cpu); spin_unlock(&global_bh_lock); return; resched_unlock: spin_unlock(&global_bh_lock); resched: mark_bh(nr); } 在软中断初始化时,将bh_action()放到bh_task_vec[32]中,bh_task_vec[32]中元素的类型是tasklet_struct,系统使用mark_bh()或task_hi_schedule()函数将它挂到task_hi_vec[]的对列中,在系统调用do_softirq()时执行。 static inline void mark_bh(int nr) { tasklet_hi_schedule(bh_task_vec+nr); } static inline void tasklet_hi_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); t->;next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_restore(flags); } } [softirq] softirq_vec[32]; struct softirq_action { void (*action)(struct softirq_action *); void *data; }; 软中断对应一个softirq_action的结构,在do_softirq()中调用相应的action()做处理。 软中断初始化时只设置了0,3两项,对应的action是task_hi_action和task_action. 1: task_hi_action /\ | tasklet_hi_vec[NR_CPU] struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned; struct tasklet_head { struct tasklet_struct *list; } __attribute__ ((__aligned__(SMP_CACHE_BYTES))); task_hi_action处理的对象是一个tasklet的队列,每个cpu都有一个对应的tasklet队列, 它在tasklet_hi_schedule中动态添加。 3: task_action /\ | tasklet_vec[NR_CPU] [tasklet] struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; 从上面的分析来看tasklet只是一个调用实体,在do_softirq()中被调用。softirq的组织和结构才是最重要的。 [目录] -------------------------------------------------------------------------------- 硬中断 标题 Linux设备驱动程序的中断 作者 coly (journeyman) 时间 07/02/01 11:24 AM Linux设备驱动程序的中断 Coly V0.1 指定参考书:《Linux设备驱动程序》(第一版) 这里总结一下Linux设备驱动程序中涉及的中断机制。 一、前言 Linux的中断宏观分为两种:软中断和硬中断。声明一下,这里的软和硬的意思是指和软件相关以及和硬件相关,而不是软件实现的中断或硬件实现的中断。软中断就是“信号机制”。软中断不是软件中断。Linux通过信号来产生对进程的各种中断操作,我们现在知道的信号共有31个,其具体内容这里略过,感兴趣读者可参看相关参考文献[1]。 一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。 硬中断就是通常意义上的“中断处理程序”,它是直接处理由硬件发过来的中断信号的。当硬中断收到它应当处理的中断信号以后,就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。 对于软中断,我们不做讨论,那是进程调度里要考虑的事情。由于我们讨论的是设备驱动程序的中断问题,所以焦点集中在硬中断里。我们这里讨论的是硬中断,即和硬件相关的中断。 二、中断产生 要中断,是因为外设需要通知操作系统她那里发生了一些事情,但是中断的功能仅仅是一个设备报警灯,当灯亮的时候中断处理程序只知道有事情发生了,但发生了什么事情还要亲自到设备那里去看才行。也就是说,当中断处理程序得知设备发生了一个中断的时候,它并不知道设备发生了什么事情,只有当它访问了设备上的一些状态寄存器以后,才能知道具体发生了什么,要怎么去处理。 设备通过中断线向中断控制器发送高电平告诉操作系统它产生了一个中断,而操作系统会从中断控制器的状态位知道是哪条中断线上产生了中断。PC机上使用的中断控制器是8259,这种控制器每一个可以管理8条中断线,当两个8259级联的时候共可以控制15条中断线。这里的中断线是实实在在的电路,他们通过硬件接口连接到CPU外的设备控制器上。 三、IRQ 并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线勇有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源。要使用中断线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),我们也常把申请一条中断线成为申请一个IRQ或者是申请一个中断号。 IRQ是非常宝贵的,所以我们建议只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无论对IRQ的使用方式是独占还是共享,申请IRQ的过程都是一样的,分为3步: 1.将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的中断中选一个作为该设备的IRQ。 2.通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。 3.根据中断申请函数的返回值决定怎么做:如果成功了万事大吉,如果没成功则或者重新申请或者放弃申请并返回错误。 申请IRQ的过程,在参考书的配的源代码里有详细的描述,读者可以通过仔细阅读源代码中的short一例对中断号申请由深刻的理解。 四、中断处理程序 Linux中的中断处理程序很有特色,它的一个中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。之所以会有上半部和下半部之分,完全是考虑到中断处理的效率。 上半部的功能是“登记中断”。当一个中断发生时,他就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就没事情了--等待新的中断的到来。这样一来,上半部执行的速度就会很快,他就可以接受更多她负责的设备产生的中断了。上半部之所以要快,是因为它是完全屏蔽中断的,如果她不执行完,其它的中断就不能被及时的处理,只能等到这个中断处理程序执行完毕以后。所以,要尽可能多得对设备产生的中断进行服务和处理,中断处理程序就一定要快。 但是,有些中断事件的处理是比较复杂的,所以中断处理程序必须多花一点时间才能够把事情做完。可怎么样化解在短时间内完成复杂处理的矛盾呢,这时候Linux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的。下半部几乎做了中断处理程序所有的事情,因为上半部只是将下半部排到了他们所负责的设备的中断处理队列中去,然后就什么都不管了。下半部一般所负责的工作是察看设备以获得产生中断的事件信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。如果有些时间下半部不知道怎么去做,他就使用著名的鸵鸟算法来解决问题--说白了就是忽略这个事件。 由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。但是有一点一定要注意,那就是如果一个设备中断处理程序正在运行,无论她是运行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序是不能并行的。 在Linux Kernel 2.0以前,中断分为快中断和慢中断(伪中断我们这里不谈),其中快中断的下半部也是不可中断的,这样可以保证它执行的快一点。但是由于现在硬件水平不断上升,快中断和慢中断的运行速度已经没有什么差别了,所以为了提高中断例程事务处理的效率,从Linux kernel 2.0以后,中断处理程序全部都是慢中断的形式了--他们的下半部是可以被中断的。 但是,在下半部中,你也可以进行中断屏蔽--如果某一段代码不能被中断的话。你可以使用cti、sti或者是save_flag、restore_flag来实现你的想法。至于他们的用法和区别,请参看本文指定参考书中断处理部分。 进一步的细节请读者参看本文指定参考书,这里就不再所说了,详细介绍细节不是我的目的,我的目的是整理概念。 五、置中断标志位 在处理中断的时候,中断控制器会屏蔽掉原先发送中断的那个设备,直到她发送的上一个中断被处理完了为止。因此如果发送中断的那个设备载中断处理期间又发送了一个中断,那么这个中断就被永远的丢失了。 之所以发生这种事情,是因为中断控制器并不能缓冲中断信息,所以当前一个中断没有处理完以前又有新的中断到达,他肯定会丢掉新的中断的。但是这种缺陷可以通过设置主处理器(CPU)上的“置中断标志位”(sti)来解决,因为主处理器具有缓冲中断的功能。如果使用了“置中断标志位”,那么在处理完中断以后使用sti函数就可以使先前被屏蔽的中断得到服务。 六、中断处理程序的不可重入性 上一节中我们提到有时候需要屏蔽中断,可是为什么要将这个中断屏蔽掉呢?这并不是因为技术上实现不了同一中断例程的并行,而是出于管理上的考虑。之所以在中断处理的过程中要屏蔽同一IRQ来的新中断,是因为中断处理程序是不可重入的,所以不能并行执行同一个中断处理程序。在这里我们举一个例子,从这里子例中可以看出如果一个中断处理程序是可以并行的话,那么很有可能会发生驱动程序锁死的情况。当驱动程序锁死的时候,你的操作系统并不一定会崩溃,但是锁死的驱动程序所支持的那个设备是不能再使用了--设备驱动程序死了,设备也就死了。 A是一段代码,B是操作设备寄存器R1的代码,C是操作设备寄存器R2的代码。其中激发PS1的事件会使A1产生一个中断,然后B1去读R1中已有的数据,然后代码C1向R2中写数据。而激发PS2的事件会使A2产生一个中断,然后B2删除R1中的数据,然后C2读去R2中的数据。 如果PS1先产生,且当他执行到A1和B1之间的时候,如果PS2产生了,这是A2会产生一个中断,将PS2中断掉(挂到任务队列的尾部),然后删除了R1的内容。当PS2运行到C2时,由于C1还没有向R2中写数据,所以C2将会在这里被挂起,PS2就睡眠在代码C2上,直到有数据可读的时候被信号唤醒。这是由于PS1中的B2原先要读的R1中的数据被PS2中的B2删除了,所以PS1页会睡眠在B1上,直到有数据可读的时候被信号唤醒。这样一来,唤醒PS1和PS2的事件就永远不会发生了,因此PS1和PS2之间就锁死了。 由于设备驱动程序要和设备的寄存器打交道,所以很难写出可以重入的代码来,因为设备寄存器就是全局变量。因此,最简洁的办法就是禁止同一设备的中断处理程序并行,即设备的中断处理程序是不可重入的。 有一点一定要清楚:在2.0版本以后的Linux kernel中,所有的上半部都是不可中断的(上半部的操作是原子性的);不同设备的下半部可以互相中断,但一个特定的下半部不能被它自己所中断(即同一个下半部不能并)。 由于中断处理程序要求不可重入,所以程序员也不必为编写可重入的代码而头痛了。以我的经验,编写可重入的设备驱动程序是可以的,编写可重入的中断处理程序是非常难得,几乎不可能。 七、避免竞争条件的出现 我们都知道,一旦竞争条件出现了,就有可能会发生死锁的情况,严重时可能会将整个系统锁死。所以一定要避免竞争条件的出现。这里我不多说,大家只要注意一点:绝大多数由于中断产生的竞争条件,都是在带有中断的 内核进程被睡眠造成的。所以在实现中断的时候,一定要相信谨慎的让进程睡眠,必要的时候可以使用cli、sti或者save_flag、restore_flag。具体细节请参看本文指定参考书。 八、实现 如何实现驱动程序的中断例程,是各位读者的事情了。只要你们仔细的阅读short例程的源代码,搞清楚编写驱动程序中断例程的规则,就可以编写自己的中断例程了。只要概念正确, 在正确的规则下编写你的代码,那就是符合道理的东西。我始终强调,概念是第一位的,能编多少代码是很其次的,我们一定要概念正确,才能进行正确的思考。 九、小结 本文介绍了Linux驱动程序中的中断,如果读者已经新痒了的话,那么打开机器开始动手吧! Time for you to leave! 参考文献: 1.Linux网络编程 2.编程之道 3.Linux设备驱动程序 4.Mouse drivers 5.Linux Kernel Hacking Guide 6.Unreliable Guide To Hacking The Linux Kernel [目录] -------------------------------------------------------------------------------- 定时器代码分析 时钟和定时器中断 IRQ 0 [Timer] | \|/ |IRQ0x00_interrupt // wrapper IRQ handler |SAVE_ALL --- |do_IRQ | wrapper routines |handle_IRQ_event --- |handler() ->; timer_interrupt // registered IRQ 0 handler |do_timer_interrupt |do_timer |jiffies++; |update_process_times |if (--counter <= 0) { // if time slice ended then |counter = 0; // reset counter |need_resched = 1; // prepare to reschedule |} |do_softirq |while (need_resched) { // if necessary |schedule // reschedule |handle_softirq |} |RESTORE_ALL ·IRQ0x00_interrupt, SAVE_ALL [include/asm/hw_irq.h] ·do_IRQ, handle_IRQ_event [arch/i386/kernel/irq.c] ·timer_interrupt, do_timer_interrupt [arch/i386/kernel/time.c] ·do_timer, update_process_times [kernel/timer.c] ·do_softirq [kernel/soft_irq.c] ·RESTORE_ALL, while loop [arch/i386/kernel/entry.S] 系统启动核心时,调用start_kernal()继续各方面的初始化,在这之前,各种中断都被禁止,只有在完成必要的初始化后,直到执行完Kmalloc_init()后,才允许中断(init\main.c)。与时钟中断有关的部分初始化如下: 调用trap_init()设置各种trap入口,如system_call、GDT entry、LDT entry、call gate等。其中0~17为各种错误入口,18~47保留。 调用init_IRQ()函数设置核心系统的时钟周期为10ms,即100HZ,它是以后按照轮转法进行CPU调度时所依照的基准时钟周期。每10ms产生的时钟中断信号直接输入到第一块8259A的INT 0(即irq0)。初始化中断矢量表中从0x20起的17个中断矢量,用bad_IRQ#_interrupt函数的地址(#为中断号)填写。 调用sched_init()函数,设置启动第一个进程init_task。设置用于管理bottom_half机制的数据结构bh_base[],规定三类事件的中断处理函数,即时钟TIMER_BH、设备TQUEUE_BH和IMMEDIATE_BH。 调用time_init()函数,首先读取当时的CMOS时间,最后调用setup_x86_irq(0,&irq0)函数,把irq0挂到irq_action[0]队列的后面,并把中断矢量表中第0x20项,即timer中断对应的中断矢量改为IRQ0_interrupt函数的地址,在irq0中,指定时间中断服务程序是timer_interrupt, static struct irqaction irq0 = { timer_interrupt, 0, 0, "timer", NULL, NULL} 结构irqaction的定义如下: struct irqaction { void (*handler)(int, void *, struct pt_regs *); /* 中断服务函数入口 */ unsigned long flags; /* 服务允中与否标记 */ unsigned long mask; const char *name; void *dev_id; struct irqaction *next; }; 其中,若flag==SA_INTERRUPT,则中断矢量改为fast_IRQ#_interrupt,在执行中断服务的过程中不允许出现中断,若为其它标记,则中断矢量为IRQ#_interrupt,在执行中断服务的过程中,允许出现中断。 Irq_action的定义与初始化如下: static void (*interrupt[17])(void) = {IRQ#_interrupt}; static void (*fast_interrupt[16])(void) = {fast_IRQ#_interrupt}; static void (*bad_interrupt[16])(void) = {bad_IRQ#_interrupt};(以上#为中断号) static struct irqaction *irq_action[16] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; irq_action是一个全局数组,每个元素指向一个irq队列,共16个irq队列,时钟中断请求队列在第一个队列,即irq_action[0]。当每个中断请求到来时,都调用setup_x86_irq把该请求挂到相应的队列的后面。 以后,系统每10ms产生一次时钟中断信号,该信号直接输入到第一块8259A的INT 0(即irq0)。CPU根据中断矢量表和中断源,找到中断矢量函数入口IRQ0_interrupt(程序运行过程中允许中断)或者fast_IRQ0_interrupt(程序运行过程中不允许中断)或者bad_IRQ0_interrupt(不执行任何动作,直接返回),这些函数由宏BUILD_TIMER_IRQ(chip, nr, mask)展开定义。 宏BUILD_TIMER_IRQ(chip, nr, mask)的定义如下: #define BUILD_TIMER_IRQ(chip,nr,mask) \ asmlinkage void IRQ_NAME(nr); \ asmlinkage void FAST_IRQ_NAME(nr); \ asmlinkage void BAD_IRQ_NAME(nr); \ __asm__( \ "\n"__ALIGN_STR"\n" \ SYMBOL_NAME_STR(fast_IRQ) #nr "_interrupt:\n\t" \ SYMBOL_NAME_STR(bad_IRQ) #nr "_interrupt:\n\t" \ SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ "pushl $-"#nr"-2\n\t" \ SAVE_ALL \ ENTER_KERNEL \ ACK_##chip(mask,(nr&7)) \ "incl "SYMBOL_NAME_STR(intr_count)"\n\t"\ /* intr_count为进入临界区的同步信号量 */ "movl %esp,%ebx\n\t" \ "pushl %ebx\n\t" \ "pushl $" #nr "\n\t" \ /* 把do_irq函数参数压进堆栈 */ "call "SYMBOL_NAME_STR(do_IRQ)"\n\t" \ "addl $8,%esp\n\t" \ "cli\n\t" \ UNBLK_##chip(mask) \ "decl "SYMBOL_NAME_STR(intr_count)"\n\t" \ "incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \ "jmp ret_from_sys_call\n"; 其中nr为中断请求类型,取值0~15。在irq.c中通过语句BUILD_TIMER_IRQ(first, 0, 0x01)调用该宏,在执行宏的过程中处理时钟中断响应程序do_irq()。 函数do_irq()的第一个参数是中断请求队列序号,时钟中断请求传进来的该参数是0。于是程序根据参数0找到请求队列irq_action[0],逐个处理该队列上handler所指的时钟中断请求的服务函数。由于已经指定时钟中断请求的服务函数是timer_interrupt,在函数timer_interrupt中,立即调用do_timer()函数。 函数do_timer()把jiffies和lost_ticks加1,接着就执行mark_bh(TIMER_BH)函数,把bottom_half中时钟队列对应的位置位,表示该队列处于激活状态。在做完这些动作后,程序从函数do_irq()中返回,继续执行以后的汇编代码。于是,程序在执行语句jmp ret_from_sys_call后,跳到指定的位置处继续执行。 代码段jmp ret_from_sys_call及其相关的代码段如下: ALIGN .globl ret_from_sys_call ret_from_sys_call: cmpl $0,SYMBOL_NAME(intr_count) jne 2f 9: movl SYMBOL_NAME(bh_mask),%eax andl SYMBOL_NAME(bh_active),%eax jne handle_bottom_half #ifdef __SMP__ cmpb $(NO_PROC_ID), SYMBOL_NAME(saved_active_kernel_processor) jne 2f #endif movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are testl $(VM_MASK),%eax # different then jne 1f cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ? je 2f 1: sti orl $(IF_MASK),%eax # these just try to make sure andl $~NT_MASK,%eax # the program doesn't do anything movl %eax,EFLAGS(%esp) # stupid cmpl $0,SYMBOL_NAME(need_resched) jne reschedule #ifdef __SMP__ GET_PROCESSOR_OFFSET(%eax) movl SYMBOL_NAME(current_set)(,%eax), %eax #else movl SYMBOL_NAME(current_set),%eax #endif cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals je 2f movl blocked(%eax),%ecx movl %ecx,%ebx # save blocked in %ebx for signal handling notl %ecx andl signal(%eax),%ecx jne signal_return 2: RESTORE_ALL ALIGN signal_return: movl %esp,%ecx pushl %ecx testl $(VM_MASK),EFLAGS(%ecx) jne v86_signal_return pushl %ebx call SYMBOL_NAME(do_signal) popl %ebx popl %ebx RESTORE_ALL ALIGN v86_signal_return: call SYMBOL_NAME(save_v86_state) movl %eax,%esp pushl %eax pushl %ebx call SYMBOL_NAME(do_signal) popl %ebx popl %ebx RESTORE_ALL handle_bottom_half: incl SYMBOL_NAME(intr_count) call SYMBOL_NAME(do_bottom_half) decl SYMBOL_NAME(intr_count) jmp 9f ALIGN reschedule: pushl $ret_from_sys_call jmp SYMBOL_NAME(schedule) # test 另外,一些与时钟中断及bottom half机制有关的数据结构介绍如下: #define HZ 100 unsigned long volatile jiffies=0; 系统每隔10ms自动把它加1,它是核心系统计时的单位。 enum { TIMER_BH = 0, CONSOLE_BH, TQUEUE_BH, DIGI_BH, SERIAL_BH, RISCOM8_BH, SPECIALIX_BH, BAYCOM_BH, NET_BH, IMMEDIATE_BH, KEYBOARD_BH, CYCLADES_BH, CM206_BH }; 现在只定义了13个bottom half队列,将来可扩充到32个队列。 unsigned long intr_count = 0; 相当于信号量的作用。只有其等于0,才可以do_bottom_half。 int bh_mask_count[32]; 用来计算bottom half队列被屏蔽的次数。只有某队列的bh_mask_count数为0,才能enable该队列。 unsigned long bh_active = 0; bh_active是32位长整数,每一位表示一个bottom half队列,该位置1,表示该队列处于激活状态,随时准备在CPU认为合适的时候执行该队列的服务,置0则相反。 unsigned long bh_mask = 0; bh_mask也是32位长整数,每一位对应一个bottom half队列,该位置1,表示该队列可用,并把处理函数的入口地址赋给bh_base,置0则相反。 void (*bh_base[32])(void); bottom half服务函数入口地址数组。定时器处理函数拥有最高的优先级,它的地址存放在bh_base[0],总是最先执行它所指向的函数。 我们注意到,在IRQ#_interrupt和fast_IRQ#_interrupt中断函数处理返回前,都通过语句jmp ret_from_sys_call,跳到系统调用的返回处(见irq.h),如果bottom half队列不为空,则在那里做类似: if (bh_active & bh_mask) { intr_count = 1; do_bottom_half(); intr_count = 0; }(该判断的汇编代码见Entry.S) 的判断,调用do_bottom_half()函数。 在CPU调度时,通过schedule函数执行上述的判断,再调用do_bottom_half()函数。 总而言之,在下列三种时机: CPU调度时 系统调用返回前 中断处理返回前 都会作判断调用do_bottom_half函数。Do_bottom_half函数依次扫描32个队列,找出需要服务的队列,执行服务后把对应该队列的bh_active的相应位置0。由于bh_active标志中TIMER_BH对应的bit为1,因而系统根据服务函数入口地址数组bh_base找到函数timer_bh()的入口地址,并马上执行该函数,在函数timer_bh中,调用函数run_timer_list()和函数run_old_timers()函数,定时执行服务。 TVECS结构及其实现 有关TVECS结构的一些数据结构定义如下: #define TVN_BITS 6 #define TVR_BITS 8 #define TVN_SIZE (1 << TVN_BITS) #define TVR_SIZE (1 << TVR_BITS) #define TVN_MASK (TVN_SIZE - 1) #define TVR_MASK (TVR_SIZE - 1) #define SLOW_BUT_DEBUGGING_TIMERS 0 struct timer_vec { int index; struct timer_list *vec[TVN_SIZE]; }; struct timer_vec_root { int index; struct timer_list *vec[TVR_SIZE]; }; static struct timer_vec tv5 = { 0 }; static struct timer_vec tv4 = { 0 }; static struct timer_vec tv3 = { 0 }; static struct timer_vec tv2 = { 0 }; static struct timer_vec_root tv1 = { 0 }; static struct timer_vec * const tvecs[] = { (struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5 }; #define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0])) static unsigned long timer_jiffies = 0; TVECS结构是一个元素个数为5的数组,分别指向tv1,tv2,tv3,tv4,tv5的地址。其中,tv1是结构timer_vec_root的变量,它有一个index域和有256个元素的指针数组,该数组的每个元素都是一条类型为timer_list的链表。其余四个元素都是结构timer_vec的变量,它们各有一个index域和64个元素的指针数组,这些数组的每个元素也都是一条链表。 函数internal_add_timer(struct timer_list *timer) 函数代码如下: static inline void internal_add_timer(struct timer_list *timer) { /* * must be cli-ed when calling this */ unsigned long expires = timer->;expires; unsigned long idx = expires - timer_jiffies; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; insert_timer(timer, tv1.vec, i); } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >;>; TVR_BITS) & TVN_MASK; insert_timer(timer, tv2.vec, i); } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >;>; (TVR_BITS + TVN_BITS)) & TVN_MASK; insert_timer(timer, tv3.vec, i); } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >;>; (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; insert_timer(timer, tv4.vec, i); } else if (expires < timer_jiffies) { /* can happen if you add a timer with expires == jiffies, * or you set a timer to go off in the past */ insert_timer(timer, tv1.vec, tv1.index); } else if (idx < 0xffffffffUL) { int i = (expires >;>; (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; insert_timer(timer, tv5.vec, i); } else { /* Can only get here on architectures with 64-bit jiffies */ timer->;next = timer->;prev = timer; } } expires 在调用该函数之前,必须关中。对该函数的说明如下: 取出要加进TVECS的timer的激发时间(expires),算出expires与timer_jiffies的差值idx,用来决定该插到哪个队列中去。 若idx小于2^8,则取expires的第0位到第7位的值I,把timer加到tv1.vec中第I个链表的第一个表项之前。 若idx小于2^14,则取expires的第8位到第13位的值I,把timer加到tv2.vec中第I个链表的第一个表项之前。 若idx小于2^20,则取expires的第14位到第19位的值I,把timer加到tv3.vec中第I个链表的第一个表项之前。 若idx小于2^26,则取expires的第20位到第25位的值I,把timer加到tv4.vec中第I个链表的第一个表项之前。 若expires小于timer_jiffies,即idx小于0,则表明该timer到期,应该把timer放入tv1.vec中tv1.index指定的链表的第一个表项之前。 若idx小于2^32,则取expires的第26位到第32位的值I,把timer加到tv5.vec中第I个链表的第一个表项之前。 若idx大等于2^32,该情况只有在64位的机器上才有可能发生,在这种情况下,不把timer加入TVECS结构。 函数cascade_timers(struct timer_vec *tv) 该函数只是把tv->;index指定的那条链表上的所有timer调用internal_add_timer()函数进行重新调整,这些timer将放入TVECS结构中比原来位置往前移一级,比如说,tv4上的timer将放到tv3上去,tv2上的timer将放到tv1上。这种前移是由run_timer_list函数里调用cascade_timers函数的时机来保证的。然后把该条链表置空,tv->;index加1,若tv->;index等于64,则重新置为0。 函数run_timer_list() 函数代码如下: static inline void run_timer_list(void) { cli(); while ((long)(jiffies - timer_jiffies) >;= 0) { struct timer_list *timer; if (!tv1.index) { int n = 1; do { cascade_timers(tvecs[n]); } while (tvecs[n]->;index == 1 && ++n < NOOF_TVECS); } while ((timer = tv1.vec[tv1.index])) { void (*fn)(unsigned long) = timer->;function; unsigned long data = timer->;data; detach_timer(timer); timer->;next = timer->;prev = NULL; sti(); fn(data); cli(); } ++timer_jiffies; tv1.index = (tv1.index + 1) & TVR_MASK; } sti(); } 对run_timer_list函数的说明如下: 关中。 判断jiffies是否大等于timer_jiffies,若不是,goto 8。 判断tv1.index是否为0(即此时系统已经扫描过整个tv1的256个timer_list链表,又回到的第一个链表处,此时需重整TVECS结构),若是,置n为1;若不是,goto 6。 调用cascade_timers()函数把TVECS[n]中由其index指定的那条链表上的timer放到TVECS[n-1]中来。注意:调用cascade_timers()函数后,index已经加1。 判断TVECS[n]->;index是否为1,即原来为0。如果是(表明TVECS[n]上所有都已经扫描一遍,此时需对其后一级的TVECS[++n]调用cascade_timers()进行重整),把n加1,goto 4。 执行tv1.vec上由tv1->;index指定的那条链表上的所有timer的服务函数,并把该timer从链表中移走。在执行服务函数的过程中,允许中断。 timer_jiffies加1,tv1->;index加1,若tv1->;index等于256,则重新置为0,goto 2。 开中,返回。 Linux提供了两种定时器服务。一种早期的由timer_struct等结构描述,由run_old_times函数处理。另一种“新”的服务由timer_list等结构描述,由add_timer、del_timer、cascade_time和run_timer_list等函数处理。 早期的定时器服务利用如下数据结构: struct timer_struct { unsigned long expires; /*本定时器被唤醒的时刻 */ void (*fn)(void); /* 定时器唤醒后的处理函数 */ } struct timer_struct timer_table[32]; /*最多可同时启用32个定时器 */ unsigned long timer_active; /* 每位对应一定时器,置1表示启用 */ 新的定时器服务依靠链表结构突破了32个的限制,利用如下的数据结构: struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data; /* 用来存放当前进程的PCB块的指针,可作为参数传 void (*function)(unsigned long); 给function */ } 表示上述数据结构的图示如下: 在这里,顺便简单介绍一下旧的timer机制的运作情况。 系统在每次调用函数do_bottom_half时,都会调用一次函数run_old_timers()。 函数run_old_timers() 该函数处理的很简单,只不过依次扫描timer_table中的32个定时器,若扫描到的定时器已经到期,并且已经被激活,则执行该timer的服务函数。 间隔定时器itimer 系统为每个进程提供了三个间隔定时器。当其中任意一个定时器到期时,就会发出一个信号给进程,同时,定时器重新开始运作。三种定时器描述如下: ITIMER_REAL 真实时钟,到期时送出SIGALRM信号。 ITIMER_VIRTUAL 仅在进程运行时的计时,到期时送出SIGVTALRM信号。 ITIMER_PROF 不仅在进程运行时计时,在系统为进程运作而运行时它也计时,与ITIMER_VIRTUAL对比,该定时器通常为那些在用户态和核心态空间运行的应用所花去的时间计时,到期时送出SIGPROF信号。 与itimer有关的数据结构定义如下: struct timespec { long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; struct timeval { int tv_sec; /* seconds */ int tv_usec; /* microseconds */ }; struct itimerspec { struct timespec it_interval; /* timer period */ struct timespec it_value; /* timer expiration */ }; struct itimerval { struct timeval it_interval; /* timer interval */ struct timeval it_value; /* current value */ }; 这三种定时器在task_struct中定义: struct task_struct { …… unsigned long timeout; unsigned long it_real_value,it_prof_value,it_virt_value; unsigned long it_real_incr,it_prof_incr,it_virt_incr; struct timer_list real_timer; …… } 在进程创建时,系统把it_real_fn函数的入口地址赋给real_timer.function。(见sched.h) 我们小组分析了三个系统调用:sys_getitimer,sys_setitimer,sys_alarm。 在这三个系统调用中,需用到以下一些函数: 函数static int _getitimer(int which, struct itimerval *value) 该函数的运行过程大致如下: 根据传进的参数which按三种itimer分别处理: 若是ITIMER_REAL,则设置interval为current进程的it_real_incr,val设置为0;判断current进程的real_timer有否设置并挂入TVECS结构中,若有,设置val为current进程real_timer的expires,并把real_timer重新挂到TVECS结构中,接着把val与当前jiffies作比较,若小等于当前jiffies,则说明该real_timer已经到期,于是重新设置val为当前jiffies的值加1。最后把val减去当前jiffies的值,goto 2。 若是ITIMER_VIRTUAL,则分别设置interval,val的值为current进程的it_virt_incr、it_virt_value,goto 2。 若是ITIMER_PROF,则分别设置interval,val的值为current进程的it_prof_incr、it_prof_value,goto 2。 (2)调用函数jiffiestotv把val,interval的jiffies值转换为timeval,返回0。 函数 int _setitimer(int which, struct itimerval *value, struct itimerval *ovalue) 该函数的运行过程大致如下: 调用函数tvtojiffies把value中的interval和value转换为jiffies i 和 j。 判断指针ovalue是否为空,若空,goto ;若不空,则把由which指定类型的itimer存入ovalue中,若存放不成功,goto 4; 根据which指定的itimer按三种类型分别处理: 若是ITIMER_REAL,则从TVECS结构中取出current进程的real_timer,并重新设置current进程的it_real_value和it_real_incr为j和i。若j等于0,goto 4;若不等于0,则把当前jiffies的值加上定时器剩余时间j,得到触发时间。若i小于j,则表明I已经溢出,应该重新设为ULONG_MAX。最后把current进程的real_timer的expires设为i,把设置过的real_timer重新加入TVECS结构,goto 4。 若是ITIMER_VIRTUAL,则设置current进程的it-_virt_value和it_virt_incr为j和i。 若是ITIMER_PROF,则设置current进程的it-_prof_value和it_prof_incr为j和i。 (4)返回0。 函数verify_area(int type, const void *addr, unsigned long size) 该函数的主要功能是对以addr为始址的,长度为size的一块存储区是否有type类型的操作权利。 函数memcpy_tofs(to, from, n) 该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。 函数memcpy_fromfs(from, to, n) 该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。 函数memset((char*)&set_buffer, 0, sizeof(set_buffer)) 该函数的主要功能是把set_buffer中的内容置为0,在这里,即把it_value和it_interval置为0。 现在,我简单介绍一下这三个系统调用: 系统调用sys_getitimer(int which, struct itimerval *value) 首先,若value为NULL,则返回-EFAULT,说明这是一个bad address。 其次,把which类型的itimer取出放入get_buffer。 再次,若存放成功,再确认对value的写权利。 最后,则把get_buffer中的itimer取出,拷入value。 系统调用sys_setitimer(int which, struct itimerval *value,struct itimerval *ovalue) 首先,判断value是否为NULL,若不是,则确认对value是否有读的权利,并把set_buffer中的数据拷入value;若value为NULL,则把set_buffer中的内容置为0,即把it_value和it_interval置为0。 其次,判断ovalue是否为NULL,若不是,则确认对ovalue是否有写的权利。 再次,调用函数_setitimer设置由which指定类型的itimer。 最后,调用函数memcpy_tofs把get_buffer中的数据拷入ovalue,返回。 系统调用sys_alarm(unsigned int seconds) 该系统调用重新设置进程的real_itimer,若seconds为0,则把原先的alarm定时器删掉。并且设interval为0,故只触发一次,并把旧的real_timer存入oldalarm,并返回oldalarm。 [目录] -------------------------------------------------------------------------------- from aka [目录] -------------------------------------------------------------------------------- 硬件中断 硬件中断 硬件中断概述 中断可以用下面的流程来表示: 中断产生源 -->; 中断向量表 (idt) -->; 中断入口 ( 一般简单处理后调用相应的函数) --->;do_IRQ-->; 后续处理(软中断等工作) 具体地说,处理过程如下: 中断信号由外部设备发送到中断芯片(模块)的引脚 中断芯片将引脚的信号转换成数字信号传给CPU,例如8259主芯片引脚0发送的是0x20 CPU接收中断后,到中断向量表IDT中找中断向量 根据存在中断向量中的数值找到向量入口 由向量入口跳转到一个统一的处理函数do_IRQ 在do_IRQ中可能会标注一些软中断,在执行完do_IRQ后执行这些软中断。 下面一一介绍。 8259芯片 本文主要参考周明德《微型计算机系统原理及应用》和billpan的相关帖子 1.中断产生过程 (1)如果IR引脚上有信号,会使中断请求寄存器(Interrupt Request Register,IRR)相应的位置位,比如图中, IR3, IR4, IR5上有信号,那么IRR的3,4,5为1 (2)如果这些IRR中有一个是允许的,也就是没有被屏蔽,那么就会通过INT向CPU发出中断请求信号。屏蔽是由中断屏蔽寄存器(Interrupt Mask Register,IMR)来控制的,比如图中位3被置1,也就是IRR位3的信号被屏蔽了。在图中,还有4,5的信号没有被屏蔽,所以,会向CPU发出请求信号。 (3)如果CPU处于开中断状态,那么在执行指令的最后一个周期,在INTA上做出回应,并且关中断. (4)8259A收到回应后,将中断服务寄存器(In-Service Register)置位,而将相应的IRR复位: 8259芯片会比较IRR中的中断的优先级,如上图中,由于IMR中位3处于屏蔽状态,所以实际上只是比较IR4,I5,缺省情况下,IR0最高,依次往下,IR7最低(这种优先级可以被设置),所以上图中,ISR被设置为4. (5)在CPU发出下一个INTA信号时,8259将中断号送到数据线上,从而能被CPU接收到,这里有个问题:比如在上图中,8259获得的是数4,但是CPU需要的是中断号(并不为4),从而可以到idt找相应的向量。所以有一个从ISR的信号到中断号的转换。在Linux的设置中,4对应的中断号是0x24. (6)如果8259处于自动结束中断(Automatic End of Interrupt AEOI)状态,那么在刚才那个INTA信号结束前,8259的ISR复位(也就是清0),如果不处于这个状态,那么直到CPU发出EOI指令,它才会使得ISR复位。 2.一些相关专题 (1)从8259 在x86单CPU的机器上采用两个8259芯片,主芯片如上图所示,x86模式规定,从8259将它的INT脚与主8259的IR2相连,这样,如果从8259芯片的引脚IR8-IR15上有中断,那么会在INT上产生信号,主8259在IR2上产生了一个硬件信号,当它如上面的步骤处理后将IR2的中断传送给CPU,收到应答后,会通过CAS通知从8259芯片,从8259芯片将IRQ中断号送到数据线上,从而被CPU接收。 由此,我猜测它产生的所有中断在主8259上优先级为2,不知道对不对。 (2)关于屏蔽 从上面可以看出,屏蔽有两种方法,一种作用于CPU, 通过清除IF标记,使得CPU不去响应8259在INT上的请求。也就是所谓关中断。 另一种方法是,作用于8259,通过给它指令设置IMR,使得相应的IRR不参与ISR(见上面的(4)),被称为禁止(disable),反之,被称为允许(enable). 每次设置IMR只需要对端口0x21(主)或0xA1(从)输出一个字节即可,字节每位对应于IMR每位,例如: outb(cached_21,0x21); 为了统一处理16个中断,Linux用一个16位cached_irq_mask变量来记录这16个中断的屏蔽情况: static unsigned int cached_irq_mask = 0xffff; 为了分别对应于主从芯片的8位IMR,将这16位cached_irq_mask分成两个8位的变量: #define __byte(x,y) (((unsigned char *)&(y))[x]) #define cached_21 (__byte(0,cached_irq_mask)) #define cached_A1 (__byte(1,cached_irq_mask)) 在禁用某个irq的时候,调用下面的函数: void disable_8259A_irq(unsigned int irq){ unsigned int mask = 1 << irq; unsigned long flags; spin_lock_irqsave(&i8259A_lock, flags); cached_irq_mask |= mask; /*-- 对这16位变量设置 */ if (irq & /*-- 看是对主8259设置还是对从芯片设置 */ outb(cached_A1,0xA1); /*-- 对从8259芯片设置 */ else outb(cached_21,0x21); /*-- 对主8259芯片设置 */ spin_unlock_irqrestore(&i8259A_lock, flags); } (3)关于中断号的输出 8259在ISR里保存的只是irq的ID,但是它告诉CPU的是中断向量ID,比如ISR保存时钟中断的ID 0,但是在通知CPU却是中断号0x20.因此需要建立一个映射。在8259芯片产生的IRQ号必须是连续的,也就是如果irq0对应的是中断向量0x20,那么irq1对应的就是0x21,... 在i8259.c/init_8259A()中,进行设置: outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */ outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */ if (auto_eoi) outb_p(0x03, 0x21); /* master does Auto EOI */ else outb_p(0x01, 0x21); /* master expects normal EOI */ outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */ outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */ outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be investigated) */ 这样,在IDT的向量0x20-0x2f可以分别填入相应的中断处理函数的地址了。 i386中断门描述符 段选择符和偏移量决定了中断处理函数的入口地址 在这里段选择符指向内核中唯一的一个代码段描述符的地址__KERNEL_CS(=0x10),而这个描述符定义的段为0到4G: --------------------------------------------------------------------------------- ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* not used */ .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ ... ... --------------------------------------------------------------------------------- 而偏移量就成了绝对的偏移量了,在IDT的描述符中被拆成了两部分,分别放在头和尾。 P标志着这个代码段是否在内存中,本来是i386提供的类似缺页的机制,在Linux中这个已经不用了,都设成1(当然内核代码是永驻内存的,但即使不在内存,推测linux也只会用缺页的标志)。 DPL在这里是0级(特权级) 0D110中,D为1,表明是32位程序(这个细节见i386开发手册).110是中断门的标识,其它101是任务门的标识, 111是陷阱(trap)门标识。 Linux对中断门的设置 于是在Linux中对硬件中断的中断门的设置为: init_IRQ(void) --------------------------------------------------------- for (i = 0; i < NR_IRQS; i++) { int vector = FIRST_EXTERNAL_VECTOR + i; if (vector != SYSCALL_VECTOR) set_intr_gate(vector, interrupt[ i]); } ---------------------------------------------------------- 其中,FIRST_EXTERNAL_VECTOR=0x20,恰好为8259芯片的IR0的中断门(见8259部分),也就是时钟中断的中断门),interrupt[ i]为相应处理函数的入口地址 NR_IRQS=224, =256(IDT的向量总数)-32(CPU保留的中断的个数),在这里设置了所有可设置的向量。 SYSCALL_VECTOR=0x80,在这里意思是避开系统调用这个向量。 而set_intr_gate的定义是这样的: ---------------------------------------------------- void set_intr_gate(unsigned int n, void *addr){ _set_gate(idt_table+n,14,0,addr); } ---------------------------------------------------- 其中,需要解释的是:14是标识指明这个是中断门,注意上面的0D110=01110=14;另外,0指明的是DPL. 中断入口 以8259的16个中断为例: 通过宏BUILD_16_IRQS(0x0), BI(x,y),以及 #define BUILD_IRQ(nr) \ asmlinkage void IRQ_NAME(nr); \ __asm__( \ "\n"__ALIGN_STR"\n" \ SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ "pushl $"#nr"-256\n\t" \ "jmp common_interrupt"; 得到的16个中断处理函数为: IRQ0x00_interrupt: push $0x00 - 256 jump common_interrupt IRQ0x00_interrupt: push $0x01 - 256 jump common_interrupt ... ... IRQ0x0f_interrupt: push $0x0f - 256 jump common_interrupt 这些处理函数简单的把中断号-256(为什么-256,也许是避免和内部中断的中断号有冲突)压到栈中,然后跳到common_interrupt 其中common_interrupt是由宏BUILD_COMMON_IRQ()展开: #define BUILD_COMMON_IRQ() \ asmlinkage void call_do_IRQ(void); \ __asm__( \ "\n" __ALIGN_STR"\n" \ "common_interrupt:\n\t" \ SAVE_ALL \ "pushl $ret_from_intr\n\t" \ SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \ "jmp "SYMBOL_NAME_STR(do_IRQ)); .align 4,0x90common_interrupt: SAVE_ALL展开的保护现场部分 push $ret_from_intrcall do_IRQ: jump do_IRQ; 从上面可以看出,这16个的中断处理函数不过是把中断号-256压入栈中,然后保护现场,最后调用do_IRQ .在common_interrupt中,为了使do_IRQ返回到entry.S的ret_from_intr标号,所以采用的是压入返回点ret_from_intr,用jump来模拟一个从ret_from_intr上面对do_IRQ的一个调用。 和IDT的衔接 为了便于IDT的设置,在数组interrupt中填入所有中断处理函数的地址: void (*interrupt[NR_IRQS])(void) = { IRQ0x00_interrupt, IRQ0x01_interrupt, ... ... } 在中断门的设置中,可以看到是如何利用这个数组的。 硬件中断处理函数do_IRQ do_IRQ的相关对象 在do_IRQ中,一个中断主要由三个对象来完成 其中, irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用). 当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。 irq_desc有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。 irq_desc还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。 例如,硬盘1,硬盘2都产生中断IRQx,在do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(对8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。 hw_irq_controller hw_irq_controller有多种: 1.在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是i8259A_irq_type 2.在多CPU的机器上,采用APIC子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过 APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。 中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种: ioapic_level_irq_type ioapic_edge_irq_type (这里指的是intel的APIC,还有其它公司研制的APIC,我没有研究过) 3. Local APIC自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller: lapic_irq_type struct hw_interrupt_type { const char * typename; unsigned int (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*enable)(unsigned int irq); void (*disable)(unsigned int irq); void (*ack)(unsigned int irq); void (*end)(unsigned int irq); void (*set_affinity)(unsigned int irq, unsigned long mask); }; typedef struct hw_interrupt_type hw_irq_controller; startup 是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽 shutdown 反之,使得芯片不再接收中断 enable 设某个引脚可以接收中断,也就是取消屏蔽 disable 屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生 ack 当CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下 (8259, APIC电平,边沿)的处理都不相同 end 在CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。 irqaction 将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构): ----------------------------------------------------- int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char * devname, void *dev_id) ----------------------------------------------------- 参数说明在源文件里说得非常清楚。 handler是硬件处理函数,在下面的代码中可以看得很清楚: --------------------------------------------- do { status |= action->;flags; action->;handler(irq, action->;dev_id, regs); action = action->;next; } while (action); --------------------------------------------- 第二个参数就是action的dev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。 第三个参数是在entry.S中压入的各个积存器的值。 它的大致流程是: 1.在slab中分配一个irqaction,填上必需的数据 以下在函数setup_irq中。 2.找到它的irq对应的结构irq_desc 3.看它是否想对随机数做贡献 4.看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了) 5.挂到队列最后 6.如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作 7在proc/下面登记 register_irq_proc(irq)(这个我不太明白) 将一个处理函数取下: void free_irq(unsigned int irq, void *dev_id) 首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。 将它从队列里剔除。 如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断: if (!desc->;action) { desc->;status |= IRQ_DISABLED; desc->;handler->;shutdown(irq); } 如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它: #ifdef CONFIG_SMP /* Wait to make sure it's not being used on another CPU */ while (desc->;status & IRQ_INPROGRESS) barrier(); #endif kfree(action); do_IRQ asmlinkage unsigned int do_IRQ(struct pt_regs regs) 1.首先取中断号,并且获取对应的irq_desc: int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */ int cpu = smp_processor_id(); irq_desc_t *desc = irq_desc + irq; 2.对中断芯片(模块)应答: desc->;handler->;ack(irq); 3.修改它的状态(注:这些状态我觉得只有在SMP下才有意义): status = desc->;status & ~(IRQ_REPLAY | IRQ_WAITING); status |= IRQ_PENDING; /* we _want_ to handle it */ IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY。 IRQ_WAITING 探测用,探测时,会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。 IRQ_PENDING , IRQ_INPROGRESS是为了确保: 同一个中断号的处理程序不能重入 不能丢失这个中断号的下一个处理程序 具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INPROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给CPU,那么当内核打算再次运行这个中断号对应的处理程序(链)时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING, 然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。 这些状态的操作不是在什么情况下都必须的,事实上,一个CPU,用8259芯片,无论即使是开中断,也不会发生中断重入的情况,因为在这期间,内核把同一中断屏蔽掉了。 多个CPU比较复杂,因为CPU由Local APIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个CPU都可能产生,它们都会调用时钟中断处理函数。 从I/O APIC传过来的中断,如果是电平触发,也不会,因为在结束发出EOI前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么I/O APIC选择不同的CPU,在这两种情况下,会有重入的可能。 /* * If the IRQ is disabled for whatever reason, we cannot * use the action we have. */ action = NULL; if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) { action = desc->;action; status &= ~IRQ_PENDING; /* we commit to handling */ status |= IRQ_INPROGRESS; /* we are handling it *//*进入执行状态*/ } desc->;status = status; /* * If there is no IRQ handler or it was disabled, exit early. Since we set PENDING, if another processor is handling a different instance of this same irq, the other processor will take care of it. */ if (!action) goto out;/*要么该中断没有处理函数;要么被禁止运行(IRQ_DISABLE);要么有一个实例已经在运行了*/ /* * Edge triggered interrupts need to remember * pending events. * This applies to any hw interrupts that allow a second * instance of the same irq to arrive while we are in do_IRQ * or in the handler. But the code here only handles the _second_ * instance of the irq, not the third or fourth. So it is mostly * useful for irq hardware that does not mask cleanly in an * SMP environment. */ for (; { spin_unlock(&desc->;lock); handle_IRQ_event(irq, ®s, action);/*执行函数链*/ spin_lock(&desc->;lock); if (!(desc->;status & IRQ_PENDING))/*发现期间有中断,就再次执行*/ break; desc->;status &= ~IRQ_PENDING; } desc->;status &= ~IRQ_INPROGRESS;/*退出执行状态*/ out: /* * The ->;end() handler has to deal with interrupts which got * disabled while the handler was running. */ desc->;handler->;end(irq);/*给中断芯片一个结束的操作,一般是允许再次接收中断*/ spin_unlock(&desc->;lock); if (softirq_active(cpu) & softirq_mask(cpu)) do_softirq();/*执行软中断*/ return 1; } [目录] -------------------------------------------------------------------------------- 软中断 软中断softirq softirq简介 提出softirq的机制的目的和老版本的底半部分的目的是一致的,都是将某个中断处理的一部分任务延迟到后面去执行。 Linux内核中一共可以有32个softirq,每个softirq实际上就是指向一个函数。当内核执行softirq(do_softirq),就对这32个softirq进行轮询: (1)是否该softirq被定义了,并且允许被执行? (2)是否激活了(也就是以前有中断要求它执行)? 如果得到肯定的答复,那么就执行这个softirq指向的函数。 值得一提的是,无论有多少个CPU,内核一共只有32个公共的softirq,但是每个CPU可以执行不同的softirq,可以禁止/起用不同的softirq,可以激活不同的softirq,因此,可以说,所有CPU有相同的例程,但是 每个CPU却有自己完全独立的实例。 对(1)的判断是通过考察irq_stat[ cpu ].mask相应的位得到的。这里面的cpu指的是当前指令所在的cpu.在一开始,softirq被定义时,所有的cpu的掩码mask都是一样的。但是在实际运行中,每个cpu上运行的程序可以根据自己的需要调整。 对(2)的判断是通过考察irq_stat[ cpu ].active相应的位得到的. 虽然原则上可以任意定义每个softirq的函数,Linux内核为了进一步加强延迟中断功能,提出了tasklet的机制。tasklet实际上也就是一个函数。在第0个softirq的处理函数tasklet_hi_action中,我们可以看到,当执行这个函数的时候,会依次执行一个链表上所有的tasklet. 我们大致上可以把softirq的机制概括成: 内核依次对32个softirq轮询,如果遇到一个可以执行并且需要的softirq,就执行对应的函数,这些函数有可能又会执行一个函数队列。当执行完这个函数队列后,才会继续询问下一个softirq对应的函数。 挂上一个软中断 void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) { unsigned long flags; int i; spin_lock_irqsave(&softirq_mask_lock, flags); softirq_vec[nr].data = data; softirq_vec[nr].action = action; for (i=0; i<NR_CPUS; i++) softirq_mask(i) |= (1<<nr); spin_unlock_irqrestore(&softirq_mask_lock, flags); } 其中对每个CPU的softirq_mask都标注一下,表明这个softirq被定义了。 tasklet 在这个32个softirq中,有的softirq的函数会依次执行一个队列中的tasklet tasklet其实就是一个函数。它的结构如下: struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; next 用于将tasklet串成一个队列 state 表示一些状态,后面详细讨论 count 用来禁用(count = 1 )或者启用( count = 0 )这个tasklet.因为一旦一个tasklet被挂到队列里,如果没有这个机制,它就一定会被执行。 这个count算是一个事后补救措施,万一挂上了不想执行,就可以把它置1。 func 即为所要执行的函数。 data 由于可能多个tasklet调用公用函数,因此用data可以区分不同tasklet. 如何将一个tasklet挂上 首先要初始化一个tasklet,填上相应的参数 void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->;func = func; t->;data = data; t->;state = 0; atomic_set(&t->;count, 0); } 然后调用schedule函数,注意,下面的函数仅仅是将这个tasklet挂到 TASKLET_SOFTIRQ对应的软中断所执行的tasklet队列上去, 事实上,还有其它的软中断,比如HI_SOFTIRQ,会执行其它的tasklet队列,如果要挂上,那么就要调用tasklet_hi_schedule(). 如果你自己写的softirq执行一个tasklet队列,那么你需要自己写类似下面的函数。 static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); /**/ t->;next = tasklet_vec[cpu].list; /**/ tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); local_irq_restore(flags); } } 这个函数中/**/标注的句子用来挂接上tasklet, __cpu_raise_softirq用来激活TASKLET_SOFTIRQ,这样,下次执行do_softirq就会执行这个TASKLET_SOFTIRQ软中断了 __cpu_raise_softirq定义如下: static inline void __cpu_raise_softirq(int cpu, int nr) { softirq_active(cpu) |= (1<<nr); } tasklet的运行方式 我们以tasklet_action为例,来说明tasklet运行机制。事实上,还有一个函数tasklet_hi_action同样也运行tasklet队列。 首先值得注意的是,我们前面提到过,所有的cpu共用32个softirq,但是同一个softirq在不同的cpu上执行的数据是独立的,基于这个原则,tasklet_vec对每个cpu都有一个,每个cpu都运行自己的tasklet队列。 当执行一个tasklet队列时,内核将这个队列摘下来,以list为队列头,然后从list的下一个开始依次执行。这样做达到什么效果呢?在执行这个队列时,这个队列的结构是静止的,如果在运行期间,有中断产生,并且往这个队列里添加tasklet的话,将填加到tasklet_vec[cpu].list中, 注意这个时候,这个队列里的任何tasklet都不会被执行,被执行的是list接管的队列。 见/*1*//*2/之间的代码。事实上,在一个队列上同时添加和运行也是可行的,没这个简洁。 ----------------------------------------------------------------- static void tasklet_action(struct softirq_action *a) { int cpu = smp_processor_id(); struct tasklet_struct *list; /*1*/ local_irq_disable(); list = tasklet_vec[cpu].list; tasklet_vec[cpu].list = NULL; /*2*/ local_irq_enable(); while (list != NULL) { struct tasklet_struct *t = list; list = list->;next; /*3*/ if (tasklet_trylock(t)) { if (atomic_read(&t->;count) == 0) { clear_bit(TASKLET_STATE_SCHED, &t->;state); t->;func(t->;data); /* * talklet_trylock() uses test_and_set_bit that imply * an mb when it returns zero, thus we need the explicit * mb only here: while closing the critical section. */ #ifdef CONFIG_SMP /*?*/ smp_mb__before_clear_bit(); #endif tasklet_unlock(t); continue; } tasklet_unlock(t); } /*4*/ local_irq_disable(); t->;next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); /*5*/ local_irq_enable(); } } ------------------------------------------------------------- /*3*/看其它cpu是否还有同一个tasklet在执行,如果有的话,就首先将这个tasklet重新放到tasklet_vec[cpu].list指向的预备队列(见/*4*/~/*5*/),而后跳过这个tasklet. 这也就说明了tasklet是不可重入的,以防止两个相同的tasket访问同样的变量而产生竞争条件(race condition) tasklet的状态 在tasklet_struct中有一个属性state,用来表示tasklet的状态: tasklet的状态有3个: 1.当tasklet被挂到队列上,还没有执行的时候,是 TASKLET_STATE_SCHED 2.当tasklet开始要被执行的时候,是 TASKLET_STATE_RUN 其它时候,则没有这两个位的设置 其实还有另一对状态,禁止或允许,tasklet_struct中用count表示,用下面的函数操作 ----------------------------------------------------- static inline void tasklet_disable_nosync(struct tasklet_struct *t) { atomic_inc(&t->;count); } static inline void tasklet_disable(struct tasklet_struct *t) { tasklet_disable_nosync(t); tasklet_unlock_wait(t); } static inline void tasklet_enable(struct tasklet_struct *t) { atomic_dec(&t->;count); } ------------------------------------------------------- 下面来验证1,2这两个状态: 当被挂上队列时: 首先要测试它是否已经被别的cpu挂上了,如果已经在别的cpu挂上了,则不再将它挂上,否则设置状态为TASKLET_STATE_SCHED static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) { ... ... } 为什么要这样做?试想,如果一个tasklet已经挂在一队列上,内核将沿着这个队列一个个执行,现在如果又被挂到另一个队列上,那么这个tasklet的指针指向另一个队列,内核就会沿着它走到错误的队列中去了。 tasklet开始执行时: 在tasklet_action中: ------------------------------------------------------------ while (list != NULL) { struct tasklet_struct *t = list; /*0*/ list = list->;next; /*1*/ if (tasklet_trylock(t)) { /*2*/ if (atomic_read(&t->;count) == 0) { /*3*/ clear_bit(TASKLET_STATE_SCHED, &t->;state); t->;func(t->;data); /* * talklet_trylock() uses test_and_set_bit that imply * an mb when it returns zero, thus we need the explicit * mb only here: while closing the critical section. */ #ifdef CONFIG_SMP smp_mb__before_clear_bit(); #endif /*4*/ tasklet_unlock(t); continue; } --------------------------------------------------------------- 1 看是否是别的cpu上这个tasklet已经是 TASKLET_STATE_RUN了,如果是就跳过这个tasklet 2 看这个tasklet是否被允许运行? 3 清除TASKLET_STATE_SCHED,为什么现在清除,它不是还没有从队列上摘下来吗?事实上,它的指针已经不再需要的,它的下一个tasklet已经被list记录了(/*0*/)。这样,如果其它cpu把它挂到其它的队列上去一点影响都没有。 4 清除TASKLET_STATE_RUN标志 1和4确保了在所有cpu上,不可能运行同一个tasklet,这样在一定程度上确保了tasklet对数据操作是安全的,但是不要忘了,多个tasklet可能指向同一个函数,所以仍然会发生竞争条件。 可能会有疑问:假设cpu 1上已经有tasklet 1挂在队列上了,cpu2应该根本挂不上同一个tasklet 1,怎么会有tasklet 1和它发生重入的情况呢? 答案就在/*3*/上,当cpu 1的tasklet 1已经不是TASKLET_STATE_SCHED,而它还在运行,这时cpu2完全有可能挂上同一个tasklet 1,而且使得它试图运行,这时/*1*/的判断就起作用了。 软中断的重入 一般情况下,在硬件中断处理程序后都会试图调用do_softirq执行软中断,但是如果发现现在已经有中断在运行,或者已经有软中断在运行,则 不再运行自己调用的中断。也就是说,软中断是不能进入硬件中断部分的,并且软中断在一个cpu上是不可重入的,或者说是串行化的(serialize) 其目的是避免访问同样的变量导致竞争条件的出现。在开中断的中断处理程序中不允许调用软中断可能是希望这个中断处理程序尽快结束。 这是由do_softirq中的 if (in_interrupt()) return; 保证的. 其中, #define in_interrupt() ({ int __cpu = smp_processor_id(); \ (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); }) 前者local_irq_count(_cpu): 当进入硬件中断处理程序时,handle_IRQ_event中的irq_enter(cpu, irq)会将它加1,表明又进入一个硬件中断 退出则调用irq_exit(cpu, irq) 后者local_bh_count(__cpu) : 当进入软中断处理程序时,do_softirq中的local_bh_disable()会将它加1,表明处于软中断中 local_bh_disable(); 一个例子: 当内核正在执行处理定时器的软中断时,这期间可能会发生多个时钟中断,这些时钟中断的处理程序都试图再次运行处理定时器的软中断,但是由于 已经有个软中断在运行了,于是就放弃返回。 软中断调用时机 最直接的调用: 当硬中断执行完后,迅速调用do_softirq来执行软中断(见下面的代码),这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都成功的,见前面关于重入的帖子。 ---------------------------------- |
内存 内存管理系统是操作系统中最为重要的部分,因为系统的物理内存总是少于系统所需要的内存数量。虚拟内存就是为了克服这个矛盾而采用的策略。系统的虚拟内存通过在各个进程之间共享内存而使系统看起来有多于实际内存的内存容量。 虚拟内存可以提供以下的功能: *广阔的地址空间。 系统的虚拟内存可以比系统的实际内存大很多倍。 *进程的保护。 系统中的每一个进程都有自己的虚拟地址空间。这些虚拟地址空间是完全分开的,这样一个进程的运行不会影响其他进程。并且,硬件上的虚拟内存机制是被保护的,内存不能被写入,这样可以防止迷失的应用程序覆盖代码的数据。 *内存映射。 内存映射用来把文件映射到进程的地址空间。在内存映射中,文件的内容直接连接到进程的虚拟地址空间。 *公平的物理内存分配。 内存管理系统允许系统中每一个运行的进程都可以公平地得到系统的物理内存。 *共享虚拟内存。 虽然虚拟内存允许进程拥有自己单独的虚拟地址空间,但有时可能会希望进程共享内存。 linux仅仅使用四个段 两个代表 (code 和 data/stack)是内核空间从[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB) 两个代表 (code 和 data/stack)是用户空间从[0] (0 GB) 到 [0xBFFF FFFF] (3 GB) __ 4 GB--->;| | | | Kernel | | 内核空间 (Code + Data/Stack) | | __| 3 GB--->;|----------------| __ | | | | | | 2 GB--->;| | | | Tasks | | 用户空间 (Code + Data/Stack) | | | 1 GB--->;| | | | | | |________________| __| 0x00000000 内核/用户 线性地址 linux可以使用3层页表映射,例如在高级的I64服务器上,但是i386体系结构下只有2层有实际意义: ------------------------------------------------------------------ 线性地址 ------------------------------------------------------------------ \___/ \___/ \_____/ PD 偏移 PF 偏移 Frame 偏移 [10 bits] [10 bits] [12 bits] | | | | | ----------- | | | | Value |----------|--------- | | | | |---------| /|\ | | | | | | | | | | | | | | | | | | Frame 偏移 | | | | | | | \|/ | | | | | |---------|<------ | | | | | | | | | | | | | | | | x 4096 | | | | PF 偏移 |_________|------- | | | | /|\ | | | PD 偏移 |_________|----- | | | _________| /|\ | | | | | | | | | | | \|/ | | \|/ _____ | | | ------>;|_________| 物理地址 | | \|/ | | x 4096 | | | CR3 |-------->;| | | | |_____| | ....... | | ....... | | | | | 页目录表 页表 Linux i386 分页 注意内核(仅仅内核)线性空间就等于内核物理空间,所以如下: ________________ _____ | 其他内核数据 |___ | | | |----------------| | |__| | | 内核 |\ |____| 实际的其他 | 3 GB --->;|----------------| \ | 内核数据 | | |\ \ | | | __|_\_\____|__ Real | | Tasks | \ \ | Tasks | | __|___\_\__|__ Space | | | \ \ | | | | \ \|----------------| | | \ | 实际内核空间 | |________________| \|________________| 逻辑地址 物理地址 [内存实时分配] |copy_mm |allocate_mm = kmem_cache_alloc |__kmem_cache_alloc |kmem_cache_alloc_one |alloc_new_slab |kmem_cache_grow |kmem_getpages |__get_free_pages |alloc_pages |alloc_pages_pgdat |__alloc_pages |rmqueue |reclaim_pages ·copy_mm [kernel/fork.c] ·allocate_mm [kernel/fork.c] ·kmem_cache_alloc [mm/slab.c] ·__kmem_cache_alloc ·kmem_cache_alloc_one ·alloc_new_slab ·kmem_cache_grow ·kmem_getpages ·__get_free_pages [mm/page_alloc.c] ·alloc_pages [mm/numa.c] ·alloc_pages_pgdat ·__alloc_pages [mm/page_alloc.c] ·rm_queue ·reclaim_pages [mm/vmscan.c] [内存交换线程kswapd] |kswapd |// initialization routines |for (; { // Main loop |do_try_to_free_pages |recalculate_vm_stats |refill_inactive_scan |run_task_queue |interruptible_sleep_on_timeout // we sleep for a new swap request |} ·kswapd [mm/vmscan.c] ·do_try_to_free_pages ·recalculate_vm_stats [mm/swap.c] ·refill_inactive_scan [mm/vmswap.c] ·run_task_queue [kernel/softirq.c] ·interruptible_sleep_on_timeout [kernel/sched.c] [内存交换机制:出现内存不足的Exception] | Page Fault Exception | cause by all these conditions: | a-) User page | b-) Read or write access | c-) Page not present | | ----------->; |do_page_fault |handle_mm_fault |pte_alloc |pte_alloc_one |__get_free_page = __get_free_pages |alloc_pages |alloc_pages_pgdat |__alloc_pages |wakeup_kswapd // We wake up kernel thread kswapd ·do_page_fault [arch/i386/mm/fault.c] ·handle_mm_fault [mm/memory.c] ·pte_alloc ·pte_alloc_one [include/asm/pgalloc.h] ·__get_free_page [include/linux/mm.h] ·__get_free_pages [mm/page_alloc.c] ·alloc_pages [mm/numa.c] ·alloc_pages_pgdat ·__alloc_pages ·wakeup_kswapd [mm/vmscan.c] [目录] -------------------------------------------------------------------------------- 内存管理子系统导读from aka 我的目标是‘导读’,提供linux内存管理子系统的整体概念,同时给出进一步深入研究某个部分时的辅助信息(包括代码组织,文件和主要函数的意义和一些参考文档)。之所以采取这种方式,是因为我本人在阅读代码的过程中,深感“读懂一段代码容易,把握整体思想却极不容易”。而且,在我写一些内核代码时,也觉得很多情况下,不一定非得很具体地理解所有内核代码,往往了解它的接口和整体工作原理就够了。当然,我个人的能力有限,时间也很不够,很多东西也是近期迫于讲座压力临时学的:),内容难免偏颇甚至错误,欢迎大家指正。 存储层次结构和x86存储管理硬件(MMU) 这里假定大家对虚拟存储,段页机制有一定的了解。主要强调一些很重要的或者容易误解的概念。 存储层次 高速缓存(cache) --〉 主存(main memory) ---〉 磁盘(disk) 理解存储层次结构的根源:CPU速度和存储器速度的差距。 层次结构可行的原因:局部性原理。 LINUX的任务: 减小footprint,提高cache命中率,充分利用局部性。 实现虚拟存储以满足进程的需求,有效地管理内存分配,力求最合理地利用有限的资源。 参考文档: 《too little,too small》by Rik Van Riel, Nov. 27,2000. 以及所有的体系结构教材:) MMU的作用 辅助操作系统进行内存管理,提供虚实地址转换等硬件支持。 x86的地址 逻辑地址: 出现在机器指令中,用来制定操作数的地址。段:偏移 线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。 物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。 LINUX: 尽量避免使用段功能以提高可移植性。如通过使用基址为0的段,使逻辑地址==线性地址。 x86的段 保护模式下的段:选择子+描述符。不仅仅是一个基地址的原因是为了提供更多的信息:保护、长度限制、类型等。描述符存放在一张表中(GDT或LDT),选择子可以认为是表的索引。段寄存器中存放的是选择子,在段寄存器装入的同时,描述符中的数据被装入一个不可见的寄存器以便cpu快速访问。(图)P40 专用寄存器:GDTR(包含全局描述附表的首地址),LDTR(当前进程的段描述附表首地址),TSR(指向当前进程的任务状态段) LINUX使用的段: __KERNEL_CS: 内核代码段。范围 0-4G。可读、执行。DPL=0。 __KERNEL_DS:内核代码段。范围 0-4G。可读、写。DPL=0。 __USER_CS:内核代码段。范围 0-4G。可读、执行。DPL=3。 __USER_DS:内核代码段。范围 0-4G。可读、写。DPL=3。 TSS(任务状态段):存储进程的硬件上下文,进程切换时使用。(因为x86硬件对TSS有一定支持,所有有这个特殊的段和相应的专用寄存器。) default_ldt:理论上每个进程都可以同时使用很多段,这些段可以存储在自己的ldt段中,但实际linux极少利用x86的这些功能,多数情况下所有进程共享这个段,它只包含一个空描述符。 还有一些特殊的段用在电源管理等代码中。 (在2.2以前,每个进程的ldt和TSS段都存在GDT中,而GDT最多只能有8192项,因此整个系统的进程总数被限制在4090左右。2。4里不再把它们存在GDT中,从而取消了这个限制。) __USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。注意不要把这个共享和进程空间的共享混淆:虽然大家使用同一个段,但通过使用不同的页表由分页机制保证了进程空间仍然是独立的。 x86的分页机制 x86硬件支持两级页表,奔腾pro以上的型号还支持Physical address Extension Mode和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常,访问完页面后自动设置accessed位等。 linux采用的是一个体系结构无关的三级页表模型(如图),使用一系列的宏来掩盖各种平台的细节。例如,通过把PMD看作只有一项的表并存储在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址),页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd),从而适应了二级页表硬件。 TLB TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是:如果操作系统更改了页表内容,它必须相应的刷新TLB以使CPU不误用过时的表项。 Cache Cache 基本上是对程序员透明的,但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化,其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section,把频繁同时使用的数据集中到一个cache行(如struct task_struct),减少一些函数的footprint,在slab分配器里头的slab coloring等。 另外,我们也必须知道什么时候cache要无效:新map/remap一页到某个地址、页面换出、页保护改变、进程切换等,也即当cache对应的那个地址的内容或含义有所变化时。当然,很多情况下不需要无效整个cache,只需要无效某个地址或地址范围即可。实际上, intel在这方面做得非常好用,cache的一致性完全由硬件维护。 关于x86处理器更多信息,请参照其手册:Volume 3: Architecture and Programming Manual 8. Linux 相关实现 这一部分的代码和体系结构紧密相关,因此大多位于arch子目录下,而且大量以宏定义和inline函数形式存在于头文件中。以i386平台为例,主要的文件包括: page.h 页大小、页掩码定义。PAGE_SIZE,PAGE_SHIFT和PAGE_MASK。 对页的操作,如清除页内容clear_page、拷贝页copy_page、页对齐page_align 还有内核虚地址的起始点:著名的PAGE_OFFSET和相关的内核中虚实地址转换的宏__pa和__va.。 virt_to_page从一个内核虚地址得到该页的描述结构struct page *.我们知道,所有物理内存都由一个memmap数组来描述。这个宏就是计算给定地址的物理页在这个数组中的位置。另外这个文件也定义了一个简单的宏检查一个页是不是合法:VALID_PAGE(page)。如果page离memmap数组的开始太远以至于超过了最大物理页面应有的距离则是不合法的。 比较奇怪的是页表项的定义也放在这里。pgd_t,pmd_t,pte_t和存取它们值的宏xxx_val pgtable.h pgtable-2level.h pgtable-3level.h 顾名思义,这些文件就是处理页表的,它们提供了一系列的宏来操作页表。pgtable-2level.h和pgtable-2level.h则分别对应x86二级、三级页表的需求。首先当然是表示每级页表有多少项的定义不同了。而且在PAE模式下,地址超过32位,页表项pte_t用64位来表示(pmd_t,pgd_t不需要变),一些对整个页表项的操作也就不同。共有如下几类: ·[pte/pmd/pgd]_ERROR 出措时要打印项的取值,64位和32位当然不一样。 ·set_[pte/pmd/pgd] 设置表项值 ·pte_same 比较 pte_page 从pte得出所在的memmap位置 ·pte_none 是否为空。 ·__mk_pte 构造pte pgtable.h的宏太多,不再一一解释。实际上也比较直观,通常从名字就可以看出宏的意义来了。pte_xxx宏的参数是pte_t,而ptep_xxx的参数是pte_t *。2.4 kernel在代码的clean up方面还是作了一些努力,不少地方含糊的名字变明确了,有些函数的可读性页变好了。 pgtable.h里除了页表操作的宏外,还有cache和tlb刷新操作,这也比较合理,因为他们常常是在页表操作时使用。这里的tlb操作是以__开始的,也就是说,内部使用的,真正对外接口在pgalloc.h中(这样分开可能是因为在SMP版本中,tlb的刷新函数和单机版本区别较大,有些不再是内嵌函数和宏了)。 pgalloc.h 包括页表项的分配和释放宏/函数,值得注意的是表项高速缓存的使用: pgd/pmd/pte_quicklist 内核中有许多地方使用类似的技巧来减少对内存分配函数的调用,加速频繁使用的分配。如buffer cache中buffer_head和buffer,vm区域中最近使用的区域。 还有上面提到的tlb刷新的接口 segment.h 定义 __KERNEL_CS[DS] __USER_CS[DS] 参考: 《Understanding the Linux Kernel》的第二章给了一个对linux 的相关实现的简要描述, 物理内存的管理。 2.4中内存管理有很大的变化。在物理页面管理上实现了基于区的伙伴系统(zone based buddy system)。区(zone)的是根据内存的不同使用类型划分的。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页等。 (实际上更高一层还有numa支持。Numa(None Uniformed Memory Access)是一种体系结构,其中对系统里的每个处理器来说,不同的内存区域可能有不同的存取时间(一般是由内存和处理器的距离决定)。而一般的机器中内存叫做DRAM,即动态随机存取存储器,对每个单元,CPU用起来是一样快的。NUMA中访问速度相同的一个内存区域称为一个Node,支持这种结构的主要任务就是要尽量减少Node之间的通信,使得每个处理器要用到的数据尽可能放在对它来说最快的Node中。2.4内核中node�相应的数据结构是pg_data_t,每个node拥有自己的memmap数组,把自己的内存分成几个zone,每个zone再用独立的伙伴系统管理物理页面。Numa要对付的问题还有很多,也远没有完善,就不多说了) 基于区的伙伴系统的设计�物理页面的管理 内存分配的两大问题是:分配效率、碎片问题。一个好的分配器应该能够快速的满足各种大小的分配要求,同时不能产生大量的碎片浪费空间。伙伴系统是一个常用的比较好的算法。(解释:TODO) 引入区的概念是为了区分内存的不同使用类型(方法?),以便更有效地利用它们。 2.4有三个区:DMA, Normal, HighMem。前两个在2.2实际上也是由独立的buddy system管理的,但2.2中还没有明确的zone的概念。DMA区在x86体系结构中通常是小于16兆的物理内存区,因为DMA控制器只能使用这一段的内存。而HighMem是物理地址超过某个值(通常是约900M)的高端内存。其他的是Normal区内存。由于linux实现的原因,高地址的内存不能直接被内核使用,如果选择了CONFIG_HIGHMEM选项,内核会使用一种特殊的办法来使用它们。(解释:TODO)。HighMem只用于page cache和用户进程。这样分开之后,我们将可以更有针对性地使用内存,而不至于出现把DMA可用的内存大量给无关的用户进程使用导致驱动程序没法得到足够的DMA内存等情况。此外,每个区都独立地监控本区内存的使用情况,分配时系统会判断从哪个区分配比较合算,综合考虑用户的要求和系统现状。2.4里分配页面时可能会和高层的VM代码交互(分配时根据空闲页面的情况,内核可能从伙伴系统里分配页面,也可能直接把已经分配的页收回�reclaim等),代码比2.2复杂了不少,要全面地理解它得熟悉整个VM工作的机理。 整个分配器的主要接口是如下函数(mm.h page_alloc.c): struct page * alloc_pages(int gfp_mask, unsigned long order) 根据gftp_mask的要求,从适当的区分配2^order个页面,返回第一个页的描述符。 #define alloc_page(gfp_mask) alloc_pages(gfp_mask,0) unsigned long __get_free_pages((int gfp_mask, unsigned long order) 工作同alloc_pages,但返回首地址。 #define __get_free_page(gfp_mask) __get_free_pages(gfp_mask,0) get_free_page 分配一个已清零的页面。 __free_page(s) 和free_page(s)释放页面(一个/多个)前者以页面描述符为参数,后者以页面地址为参数。 关于Buddy算法,许多教科书上有详细的描述,第六章对linux的实现有一个很好的介绍。关于zone base buddy更多的信息,可以参见Rik Van Riel 写的" design for a zone based memory allocator"。这个人是目前linuxmm的维护者,权威啦。这篇文章有一点过时了,98年写的,当时还没有HighMem,但思想还是有效的。还有,下面这篇文章分析2.4的实现代码: http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html。 Slab--连续物理区域管理 单单分配页面的分配器肯定是不能满足要求的。内核中大量使用各种数据结构,大小从几个字节到几十上百k不等,都取整到2的幂次个页面那是完全不现实的。2.0的内核的解决方法是提供大小为2,4,8,16,...,131056字节的内存区域。需要新的内存区域时,内核从伙伴系统申请页面,把它们划分成一个个区域,取一个来满足需求;如果某个页面中的内存区域都释放了,页面就交回到伙伴系统。这样做的效率不高。有许多地方可以改进: 不同的数据类型用不同的方法分配内存可能提高效率。比如需要初始化的数据结构,释放后可以暂存着,再分配时就不必初始化了。 内核的函数常常重复地使用同一类型的内存区,缓存最近释放的对象可以加速分配和释放。 对内存的请求可以按照请求频率来分类,频繁使用的类型使用专门的缓存,很少使用的可以使用类似2.0中的取整到2的幂次的通用缓存。 使用2的幂次大小的内存区域时高速缓存冲突的概率较大,有可能通过仔细安排内存区域的起始地址来减少高速缓存冲突。 缓存一定数量的对象可以减少对buddy系统的调用,从而节省时间并减少由此引起的高速缓存污染。 2.2实现的slab分配器体现了这些改进思想。 主要数据结构 接口: kmem_cache_create/kmem_cache_destory kmem_cache_grow/kmem_cache_reap 增长/缩减某类缓存的大小 kmem_cache_alloc/kmem_cache_free 从某类缓存分配/释放一个对象 kmalloc/kfree 通用缓存的分配、释放函数。 相关代码(slab.c)。 相关参考: http://www.lisoleg.net/lisoleg/memory/slab.pdf :Slab发明者的论文,必读经典。 第六章,具体实现的详细清晰的描述。 AKA2000年的讲座也有一些大虾讲过这个主题,请访问aka主页:www.aka.org.cn vmalloc/vfree �物理地址不连续,虚地址连续的内存管理 使用kernel页表。文件vmalloc.c,相对简单。 2.4内核的VM(完善中。。。) 进程地址空间管理 创建,销毁。 mm_struct, vm_area_struct, mmap/mprotect/munmap page fault处理,demand page, copy on write 相关文件: include/linux/mm.h:struct page结构的定义,page的标志位定义以及存取操作宏定义。struct vm_area_struct定义。mm子系统的函数原型说明。 include/linux/mman.h:和vm_area_struct的操作mmap/mprotect/munmap相关的常量宏定义。 memory.c:page fault处理,包括COW和demand page等。 对一个区域的页表相关操作: zeromap_page_range: 把一个范围内的页全部映射到zero_page remap_page_range:给定范围的页重新映射到另一块地址空间。 zap_page_range:把给定范围内的用户页释放掉,页表清零。 mlock.c: mlock/munlock系统调用。mlock把页面锁定在物理内存中。 mmap.c::mmap/munmap/brk系统调用。 mprotect.c: mprotect系统调用。 前面三个文件都大量涉及vm_area_struct的操作,有很多相似的xxx_fixup的代码,它们的任务是修补受到影响的区域,保证vm_area_struct 链表正确。 交换 目的: 使得进程可以使用更大的地址空间。同时容纳更多的进程。 任务: 选择要换出的页 决定怎样在交换区中存储页面 决定什么时候换出 kswapd内核线程:每10秒激活一次 任务:当空闲页面低于一定值时,从进程的地址空间、各类cache回收页面 为什么不能等到内存分配失败再用try_to_free_pages回收页面?原因: 有些内存分配时在中断或异常处理调用,他们不能阻塞 有时候分配发生在某个关键路径已经获得了一些关键资源的时候,因此它不能启动IO。如果不巧这时所有的路径上的内存分配都是这样,内存就无法释放。 kreclaimd 从inactive_clean_list回收页面,由__alloc_pages唤醒。 相关文件: mm/swap.c kswapd使用的各种参数以及操作页面年龄的函数。 mm/swap_file.c 交换分区/文件的操作。 mm/page_io.c 读或写一个交换页。 mm/swap_state.c swap cache相关操作,加入/删除/查找一个swap cache等。 mm/vmscan.c 扫描进程的vm_area,试图换出一些页面(kswapd)。 reclaim_page:从inactive_clean_list回收一个页面,放到free_list kclaimd被唤醒后重复调用reclaim_page直到每个区的 zone->;free_pages>;= zone->;pages_low page_lauder:由__alloc_pages和try_to_free_pages等调用。通常是由于freepages + inactive_clean_list的页太少了。功能:把inactive_dirty_list的页面转移到inactive_clean_list,首先把已经被写回文件或者交换区的页面(by bdflush)放到inactive_clean_list,如果freepages确实短缺,唤醒bdflush,再循环一遍把一定数量的dirty页写回。 关于这几个队列(active_list,inactive_dirty_list,inactive_clean_list)的逻辑,请参照:文档:RFC: design for new VM,可以从lisoleg的文档精华获得。 page cache、buffer cache和swap cache page cache:读写文件时文件内容的cache,大小为一个页。不一定在磁盘上连续。 buffer cache:读写磁盘块的时候磁盘块内容的cache,buffer cache的内容对应磁盘上一个连续的区域,一个buffer cache大小可能从512(扇区大小)到一个页。 swap cache: 是page cache的子集。用于多个进程共享的页面被换出到交换区的情况。 page cache 和 buffer cache的关系 本质上是很不同的,buffer cache缓冲磁盘块内容,page cache缓冲文件的一页内容。page cache写回时会使用临时的buffer cache来写磁盘。 bdflush: 把dirty的buffer cache写回磁盘。通常只当dirty的buffer太多或者需要更多的buffer而内存开始不足时运行。page_lauder也可能唤醒它。 kupdate: 定时运行,把写回期限已经到了的dirty buffer写回磁盘。 2.4的改进:page cache和buffer cache耦合得更好了。在2.2里,磁盘文件的读使用page cache,而写绕过page cache,直接使用buffer cache,因此带来了同步的问题:写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进,文件可以通过page cache直接写了,page cache优先使用high memory。而且,2.4引入了新的对象:file address space,它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset;原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能(这个在2.2很难实现,许多讨论过)。 虚存系统则从freeBSD借鉴了很多经验,针对2.2的问题作了巨大的调整。 文档:RFC: design for new VM不可不读。 由于时间仓促,新vm的很多细微之处我也还没来得及搞清楚。先大致罗列一下,以后我将进一步完善本文,争取把问题说清楚。另外,等这学期考试过后,我希望能为大家提供一些详细注释过的源代码。 [目录] -------------------------------------------------------------------------------- 用户态 用户空间存取内核空间,具体的实现方法要从两个方面考虑,先是用户进程,需要调用mmapp来将自己的一段虚拟空间映射到内核态分配的物理内存;然后内核空间需要重新设置用户进程的这段虚拟内存的页表,使它的物理地址指向对应的物理内存。针对linux内核的几种不同的内存分配方式(kmalloc、vmalloc和ioremap),需要进行不同的处理。 一、Linux内存管理概述 这里说一下我的理解,主要从数据结构说。 1、物理内存都是按顺序分成一页一页的,每页用一个page结构来描述。系统所有的物理页 面的page结 构描述就组成了一个数组mem_map。 2、进程的虚拟地址空间用task_struct的域mm来描述,它是一个mm_struct结构,这个结构包包含了指向? 程页目录的指针(pgd_t * pgd)和指向进程虚拟内存区域的指针(struct vm_area_structt * mmap) 3、进程虚拟内存区域具有相同属性的段用结构vm_area_struct描述(简称为VMA)。进程所所有的VMA? 树组织。 4、每个VMA就是一个对象,定义了一组操作,可以通过这组操作来对不同类型的VMA进行不屯 的处理。 例如对vmalloc分配的内存的映射就是通过其中的nopage操作实现的。 二、mmap处理过程 当用户调用mmap的时候,内核进行如下的处理: 1、先在进程的虚拟空间查找一块VMA; 2、将这块VMA去映射 3、如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它 4、将这个VMA插入到进程的VMA链中 file_operations的中定义的mmap方法原型如下: int (*mmap) (struct file *, struct vm_area_struct *); 其中file是虚拟空间映射到的文件结构,vm_area_struct就是步骤1中找到的VMA。 三、缺页故障处理过程 当访问一个无效的虚拟地址(可能是保护故障,也可能缺页故障等)的时候,就会产生一个个页故障,? 统的处理过程如下: 1、找到这个虚拟地址所在的VMA; 2、如果必要,分配中间页目录表和页表 3、如果页表项对应的物理页面不存在,则调用这个VMA的nopage方法,它返回物理页面的paage描述结构 (当然这只是其中的一种情况) 4、针对上面的情况,将物理页面的地址填充到页表中 当页故障处理完后,系统将重新启动引起故障的指令,然后就可以正常访问了 下面是VMA的方法: struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, innt write_access); }; 其中缺页函数nopage的address是引起缺页故障的虚拟地址,area是它所在的VMA,write_acccess是存取 属性。 三、具体实现 3.1、对kmalloc分配的内存的映射 对kmalloc分配的内存,因为是一段连续的物理内存,所以它可以简单的在mmap例程中设置汉 页表的物 理地址,方法是使用函数remap_page_range。它的原型如下: int remap_page_range(unsigned long from, unsigned long phys_addr, unsigned long size, pgprot_t prot) 其中from是映射开始的虚拟地址。这个函数为虚拟地址空间from和from+size之间的范围构栽 页表; phys_addr是虚拟地址应该映射到的物理地址;size是被映射区域的大小;prot是保护标志? remap_page_range的处理过程是对from到form+size之间的每一个页面,查找它所在的页目侣己 页表( 必要时建立页表),清除页表项旧的内容,重新填写它的物理地址与保护域。 remap_page_range可以对多个连续的物理页面进行处理。<<Linux设备驱动程序>;>;指出, remap_page_range只能给予对保留的页和物理内存之上的物理地址的访问,当对非保留的页页使? remap_page_range时,缺省的nopage处理控制映射被访问的虚地址处的零页。所以在分配内内存后,就? 对所分配的内存置保留位,它是通过函数mem_map_reserve实现的,它就是对相应物理页面? PG_reserved标志位。(关于这一点,参见前面的主题为“关于remap_page_range的疑问”档奶致郏? 因为remap_page_range有上面的限制,所以可以用另外一种方式,就是采用和vmalloc分配档哪 存同样 的方法,对缺页故障进行处理。 3.2、对vmalloc分配的内存的映射 3.2.1、vmalloc分配内存的过程 (1)、进行预处理和合法性检查,例如将分配长度进行页面对齐,检查分配长度是否过大? (2)、以GFP_KERNEL为优先级调用kmalloc分配(GFP_KERNEL用在进程上下文中,所以这里里就限制了? 中断处理程序中调用vmalloc)描述vmalloc分配的内存的vm_struct结构。 (3)、将size加一个页面的长度,使中间形成4K的隔离带,然后在VMALLOC_START和VMALLOOC_END之间 编历vmlist链表,寻找一段自由内存区间,将其地址填入vm_struct结构中 (4)、返回这个地址 vmalloc分配的物理内存并不连续 3.2.2、页目录与页表的定义 typedef struct { unsigned long pte_low; } pte_t; typedef struct { unsigned long pmd; } pmd_t; typedef struct { unsigned long pgd; } pgd_t; #define pte_val(x) ((x).pte_low) 3.2.3、常见例程: (1)、virt_to_phys():内核虚拟地址转化为物理地址 #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) extern inline unsigned long virt_to_phys(volatile void * address) { return __pa(address); } 上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000),因为内核空间从3G到3G+实实际内存一? 映射到物理地址的0到实际内存 (2)、phys_to_virt():内核物理地址转化为虚拟地址 #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) extern inline void * phys_to_virt(unsigned long address) { return __va(address); } virt_to_phys()和phys_to_virt()都定义在include\asm-i386\io.h中 (3)、#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >;>; PAGE_SHIFT))(内核核2.4? #define VALID_PAGE(page) ((page - mem_map) < max_mapnr)(内核2.4) 第一个宏根据虚拟地址,将其转换为相应的物理页面的page描述结构,第二个宏判断页面是是不是在有? 的物理页面内。(这两个宏处理的虚拟地址必须是内核虚拟地址,例如kmalloc返回的地址#杂? vmalloc返回的地址并不能这样,因为vmalloc分配的并不是连续的物理内存,中间可能有空空洞? 3.2.4、vmalloc分配的内存的mmap的实现: 对vmalloc分配的内存需要通过设置相应VMA的nopage方法来实现,当产生缺页故障的时候,,会调用VM 的nopage方法,我们的目的就是在nopage方法中返回一个page结构的指针,为此,需要通过过如下步骤? (1) pgd_offset_k或者 pgd_offset:查找虚拟地址所在的页目录表,前者对应内核空间档男 拟地址 ,后者对应用户空间的虚拟地址 #define pgd_offset(mm, address) ((mm)->;pgd+pgd_index(address)) #define pgd_offset_k(address) pgd_offset(&init_mm, address) 对于后者,init_mm是进程0(idle process)的虚拟内存mm_struct结构,所有进程的内核 页表都一样 。在vmalloc分配内存的时候,要刷新内核页目录表,2.4中为了节省开销,只更改了进程0档哪 核页目 录,而对其它进程则通过访问时产生页面异常来进行更新各自的内核页目录 (2)pmd_offset:找到虚拟地址所在的中间页目录项。在查找之前应该使用pgd_none判断适 否存在相 应的页目录项,这些函数如下: extern inline int pgd_none(pgd_t pgd) { return 0; } extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) { return (pmd_t *) dir; } (3)pte_offset:找到虚拟地址对应的页表项。同样应该使用pmd_none判断是否存在相应档 中间页目 录: #define pmd_val(x) ((x).pmd) #define pmd_none(x) (!pmd_val(x)) #define __pte_offset(address) \ ((address >;>; PAGE_SHIFT) & (PTRS_PER_PTE - 1)) #define pmd_page(pmd) \ ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK)) #define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \ __pte_offset(address)) (4)pte_present和pte_page:前者判断页表对应的物理地址是否有效,后者取出页表中物物理地址对? 的page描述结构 #define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE)) #define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >;>; PAGE_SHIFT)))) #define page_address(page) ((page)->;virtual) 下面的一个DEMO与上面的关系不大,它是做这样一件事情,就是在启动的时候保留一段内存存,然后使? ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样亮 边都能访 问,通过内核虚拟地址将这段内存初始化串"abcd",然后使用用户虚拟地址读出来。 /************mmap_ioremap.c**************/ #include <linux/module.h>; #include <linux/kernel.h>; #include <linux/errno.h>; #include <linux/mm.h>; #include <linux/wrapper.h>; /* for mem_map_(un)reserve */ #include <asm/io.h>; /* for virt_to_phys */ #include <linux/slab.h>; /* for kmalloc and kfree */ MODULE_PARM(mem_start,"i"; MODULE_PARM(mem_size,"i"; static int mem_start=101,mem_size=10; static char * reserve_virt_addr; static int major; int mmapdrv_open(struct inode *inode, struct file *file); int mmapdrv_release(struct inode *inode, struct file *file); int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma); static struct file_operations mmapdrv_fops = { owner: THIS_MODULE, mmap: mmapdrv_mmap, open: mmapdrv_open, release: mmapdrv_release, }; int init_module(void) { if ( ( major = register_chrdev(0, "mmapdrv", &mmapdrv_fops) ) < 0 ) { printk("mmapdrv: unable to register character device\n"; return (-EIO); } printk("mmap device major = %d\n",major ); printk( "high memory physical address 0x%ldM\n", virt_to_phys(high_memory)/1024/1024 ); reserve_virt_addr = ioremap( mem_start*1024*1024,mem_size*1024*1024); printk( "reserve_virt_addr = 0x%lx\n", (unsigned long)reserve_virt_addr ); if ( reserve_virt_addr ) { int i; for ( i=0;i<mem_size*1024*1024;i+=4) { reserve_virt_addr = 'a'; reserve_virt_addr[i+1] = 'b'; reserve_virt_addr[i+2] = 'c'; reserve_virt_addr[i+3] = 'd'; } } else { unregister_chrdev( major, "mmapdrv" ); return -ENODEV; } return 0; } /* remove the module */ void cleanup_module(void) { if ( reserve_virt_addr ) iounmap( reserve_virt_addr ); unregister_chrdev( major, "mmapdrv" ); return; } int mmapdrv_open(struct inode *inode, struct file *file) { MOD_INC_USE_COUNT; return(0); } int mmapdrv_release(struct inode *inode, struct file *file) { MOD_DEC_USE_COUNT; return(0); } int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma) { unsigned long offset = vma->;vm_pgoff<<AGE_SHIFT; unsigned long size = vma->;vm_end - vma->;vm_start; if ( size >; mem_size*1024*1024 ) { printk("size too big\n"; return(-ENXIO); } offset = offset + mem_start*1024*1024; /* we do not want to have this area swapped out, lock it */ vma->;vm_flags |= VM_LOCKED; if ( remap_page_range(vma->;vm_start,offset,size,PAGE_SHARED)) { printk("remap page range failed\n"; return -ENXIO; } return(0); } 使用LDD2源码里面自带的工具mapper测试结果如下: [root@localhost modprg]# insmod mmap_ioremap.mod mmap device major = 254 high memory physical address 0x100M reserve_virt_addr = 0xc7038000 [root@localhost modprg]# mknod mmapdrv c 254 0 [root@localhost modprg]# ./mapper mmapdrv 0 1024 | od -Ax -t x1 mapped "mmapdrv" from 0 to 1024 000000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64 * 000400 [root@localhost modprg]# [目录] -------------------------------------------------------------------------------- 内核页目录的初始化 内核页目录的初始化 内核页目录的初始化 /* swapper_pg_dir is the main page directory, address 0x00101000*/ >;>;>; 内核页目录,第0,1项和第768、767项均为映射到物理内存0-8M的页目录项 >;>;>; 其页表的物理地址是0x00102000和0x00103000,即下面的pg0和pg1所在的位置 >;>;>; (在启动的时候,将内核映像移到0x0010000处)。 >;>;>; 之所以第0,1项与第768和767相同,是因为在开启分页前的线性地址0-8M和开启 >;>;>; 分页之后的3G-3G+8M均映射到相同的物理地址0-8M /* * This is initialized to create an identity-mapping at 0-8M (for bootup * purposes) and another mapping of the 0-8M area at virtual address * PAGE_OFFSET. */ .org 0x1000 ENTRY(swapper_pg_dir) .long 0x00102007 .long 0x00103007 .fill BOOT_USER_PGD_PTRS-2,4,0 /* default: 766 entries */ .long 0x00102007 .long 0x00103007 /* default: 254 entries */ .fill BOOT_KERNEL_PGD_PTRS-2,4,0 /* * The page tables are initialized to only 8MB here - the final page * tables are set up later depending on memory size. */ >;>;>; 下面为物理地址0-8M的页表项 >;>;>; 从0x4000到0x2000共2k个页表项,映射0-8M的物理内存 .org 0x2000 ENTRY(pg0) .org 0x3000 ENTRY(pg1) /* * empty_zero_page must immediately follow the page tables ! (The * initialization loop counts until empty_zero_page) */ .org 0x4000 ENTRY(empty_zero_page) >;>;>; 进程0的页目录指向swapper_pg_dir #define INIT_MM(name) \ { \ mmap: &init_mmap, \ mmap_avl: NULL, \ mmap_cache: NULL, \ pgd: swapper_pg_dir, \ mm_users: ATOMIC_INIT(2), \ mm_count: ATOMIC_INIT(1), \ map_count: 1, \ mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \ page_table_lock: SPIN_LOCK_UNLOCKED, \ mmlist: LIST_HEAD_INIT(name.mmlist), \ } /* * paging_init() sets up the page tables - note that the first 8MB are * already mapped by head.S. * * This routines also unmaps the page at virtual kernel address 0, so * that we can trap those pesky NULL-reference errors in the kernel. */ void __init paging_init(void) { pagetable_init(); __asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir))); 。。。。。。。。。。。 } static void __init pagetable_init (void) { unsigned long vaddr, end; pgd_t *pgd, *pgd_base; int i, j, k; pmd_t *pmd; pte_t *pte, *pte_base; >;>;>; end虚拟空间的最大值(最大物理内存+3G) /* * This can be zero as well - no problem, in that case we exit * the loops anyway due to the PTRS_PER_* conditions. */ end = (unsigned long)__va(max_low_pfn*PAGE_SIZE); pgd_base = swapper_pg_dir; #if CONFIG_X86_PAE for (i = 0; i < PTRS_PER_PGD; i++) set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page))); #endif >;>;>; 内核起始虚拟空间在内核页目录表中的索引 i = __pgd_offset(PAGE_OFFSET); pgd = pgd_base + i; >;>;>; #define PTRS_PER_PGD 1024 >;>;>; 对页目录的从768项开始的每一项 for (; i < PTRS_PER_PGD; pgd++, i++) { >;>;>; vaddr为第i项页目录项所映射的内核空间的起始虚拟地址,PGDIR_SIZE=4M vaddr = i*PGDIR_SIZE; if (end && (vaddr >;= end)) break; #if CONFIG_X86_PAE pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE); set_pgd(pgd, __pgd(__pa(pmd) + 0x1)); #else >;>;>; 对两级映射机制,pmd实际上是pgd pmd = (pmd_t *)pgd; #endif if (pmd != pmd_offset(pgd, 0)) BUG(); for (j = 0; j < PTRS_PER_PMD; pmd++, j++) { vaddr = i*PGDIR_SIZE + j*PMD_SIZE; if (end && (vaddr >;= end)) break; >;>;>; 假如内核不支持 Page Size Extensions if (cpu_has_pse) { 。。。。。。。。。。 } >;>;>; 分配内核页表 pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE); >;>;>; 对每一项页表项 for (k = 0; k < PTRS_PER_PTE; pte++, k++) { vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE; if (end && (vaddr >;= end)) break; >;>;>; 将页面的物理地址填入页表项中 *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL); } >;>;>; 将页表的物理地址填入到页目录项中 set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base))); if (pte_base != pte_offset(pmd, 0)) BUG(); } } /* * Fixed mappings, only the page table structure has to be * created - mappings will be set by set_fixmap(): */ vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; fixrange_init(vaddr, 0, pgd_base); #if CONFIG_HIGHMEM 。。。。。。。。。。。。 #endif #if CONFIG_X86_PAE 。。。。。。。。。。。。 #endif } [目录] -------------------------------------------------------------------------------- 内核线程页目录的借用 创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。 >;>;>; kernel_thread以标志CLONE_VM调用clone系统调用 /* * Create a kernel thread */ int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) { long retval, d0; __asm__ __volatile__( "movl %%esp,%%esi\n\t" "int $0x80\n\t" /* Linux/i386 system call */ "cmpl %%esp,%%esi\n\t" /* child or parent? */ /* Load the argument into eax, and push it. That way, it does * not matter whether the called function is compiled with * -mregparm or not. */ "movl %4,%%eax\n\t" "pushl %%eax\n\t" "call *%5\n\t" /* call fn */ "movl %3,%0\n\t" /* exit */ "int $0x80\n" "1:\t" :"=&a" (retval), "=&S" (d0) :"0" (__NR_clone), "i" (__NR_exit), "r" (arg), "r" (fn), "b" (flags | CLONE_VM) : "memory"; return retval; } >;>;>; sys_clone->;do_fork->;copy_mm: static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) { struct mm_struct * mm, *oldmm; int retval; 。。。。。。。。 tsk->;mm = NULL; tsk->;active_mm = NULL; /* * Are we cloning a kernel thread? * * We need to steal a active VM for that.. */ >;>;>; 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL oldmm = current->;mm; if (!oldmm) return 0; >;>;>; 内核线程,只是增加当前进程的虚拟空间的引用计数 if (clone_flags & CLONE_VM) { atomic_inc(&oldmm->;mm_users); mm = oldmm; goto good_mm; } 。。。。。。。。。。 good_mm: >;>;>; 内核线程的mm和active_mm指向当前进程的mm_struct结构 tsk->;mm = mm; tsk->;active_mm = mm; return 0; 。。。。。。。 } 然后内核线程一般调用daemonize来释放对用户空间的引用: >;>;>; daemonize->;exit_mm->;_exit_mm: /* * Turn us into a lazy TLB process if we * aren't already.. */ static inline void __exit_mm(struct task_struct * tsk) { struct mm_struct * mm = tsk->;mm; mm_release(); if (mm) { atomic_inc(&mm->;mm_count); if (mm != tsk->;active_mm) BUG(); /* more a memory barrier than a real lock */ task_lock(tsk); >;>;>; 释放用户虚拟空间的数据结构 tsk->;mm = NULL; task_unlock(tsk); enter_lazy_tlb(mm, current, smp_processor_id()); >;>;>; 递减mm的引用计数并是否为0,是则释放mm所代表的映射 mmput(mm); } } asmlinkage void schedule(void) { 。。。。。。。。。 if (!current->;active_mm) BUG(); 。。。。。。。。。 prepare_to_switch(); { struct mm_struct *mm = next->;mm; struct mm_struct *oldmm = prev->;active_mm; >;>;>; mm = NULL,选中的为内核线程 if (!mm) { >;>;>; 对内核线程,active_mm = NULL,否则一定是出错了 if (next->;active_mm) BUG(); >;>;>; 选中的内核线程active_mm借用老进程的active_mm next->;active_mm = oldmm; atomic_inc(&oldmm->;mm_count); enter_lazy_tlb(oldmm, next, this_cpu); } else { >;>;>; mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了 if (next->;active_mm != mm) BUG(); switch_mm(oldmm, mm, next, this_cpu); } >;>;>; prev = NULL ,切换出去的是内核线程 if (!prev->;mm) { >;>;>; 设置其 active_mm = NULL 。 prev->;active_mm = NULL; mmdrop(oldmm); } } } 对内核线程的虚拟空间总结一下: 1、创建的时候: 父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m 父进程是内核线程,则mm和active_mm均为NULL 总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。 2、进程调度的时候 如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm; 如果切换出去的是内核线程,则置active_mm为NULL。 [目录] -------------------------------------------------------------------------------- 用户进程内核页目录的建立 用户进程内核页目录的建立 在fork一个进程的时候,必须建立进程自己的内核页目录项(内核页目录项要 与用户空间的的页目录放在同一个物理地址连续的页面上,所以不能共享,但 所有进程的内核页表与进程0共享? 3G用户,页目录中一项映射4M的空间(一项页目录1024项页表,每项页表对应1个页面4K)# 即: #define PGDIR_SHIFT 22 #define PGDIR_SIZE (1UL << PGDIR_SHIFT) >;>;>; sys_fork->;do_fork->;copy_mm->;mm_init->;pgd_alloc->;get_pgd_slow #if CONFIG_X86_PAE 。。。。。。。。。。。。。 #else extern __inline__ pgd_t *get_pgd_slow(void) { >;>;>; 分配页目录表(包含1024项页目录),即为一个进程分配的页目录可以映射的空间为10024*4M=4G pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL); if (pgd) { >;>;>; #define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE) >;>;>; TASK_SIZE为3G大小,USER_PTRS_PER_PGD为用户空间对应的页目录项数目(3G/4M=768? >;>;>; 将用户空间的页目录项清空 memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t)); >;>;>; 将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第7688项到1023项中 memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER__PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)); } return pgd; } #endif [目录] -------------------------------------------------------------------------------- 内核页目录的同步 内核页目录的同步 当一个进程在内核空间发生缺页故障的时候,在其处理程序中,就要通过0号进程的页目录览 同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如下: asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) { 。。。。。。。。 >;>;>; 缺页故障产生的地址 /* get the address */ __asm__("movl %%cr2,%0":"=r" (address)); tsk = current; /* * We fault-in kernel-space virtual memory on-demand. The * 'reference' page table is init_mm.pgd. */ >;>;>; 如果缺页故障在内核空间 if (address >;= TASK_SIZE) goto vmalloc_fault; 。。。。。。。。。 vmalloc_fault: { /* * Synchronize this task's top level page-table * with the 'reference' page table. */ int offset = __pgd_offset(address); pgd_t *pgd, *pgd_k; pmd_t *pmd, *pmd_k; pgd = tsk->;active_mm->;pgd + offset; pgd_k = init_mm.pgd + offset; >;>;>; /* >;>;>; * (pmds are folded into pgds so this doesnt get actually called, >;>;>; * but the define is needed for a generic inline function.) >;>;>; */ >;>;>; #define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval) >;>;>; #define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval) >;>;>; 如果本进程的该地址的内核页目录不存在 if (!pgd_present(*pgd)) { >;>;>; 如果进程0的该地址处的内核页目录也不存在,则出错 if (!pgd_present(*pgd_k)) goto bad_area_nosemaphore; >;>;>; 复制进程0的该地址的内核页目录到本进程的相应页目录中 set_pgd(pgd, *pgd_k); return; } >;>;>; extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) >;>;>; { >;>;>; return (pmd_t *) dir; >;>;>; } pmd = pmd_offset(pgd, address); pmd_k = pmd_offset(pgd_k, address); >;>;>; 对中间页目录,如果是两级页表,下面的几步操作与上面的重复 if (pmd_present(*pmd) || !pmd_present(*pmd_k)) goto bad_area_nosemaphore; set_pmd(pmd, *pmd_k); return; } /* * Switch to real mode and then execute the code * specified by the code and length parameters. * We assume that length will aways be less that 100! */ void machine_real_restart(unsigned char *code, int length) { 。。。。。。。。。。。。。 /* Remap the kernel at virtual address zero, as well as offset zero from the kernel segment. This assumes the kernel segment starts at virtual address PAGE_OFFSET. */ memcpy (swapper_pg_dir, swapper_pg_dir + USER_PGD_PTRS, sizeof (swapper_pg_dir [0]) * KERNEL_PGD_PTRS); /* Make sure the first page is mapped to the start of physical memory. It is normally not mapped, to trap kernel NULL pointer dereferences. */ pg0[0] = _PAGE_RW | _PAGE_PRESENT; /* * Use `swapper_pg_dir' as our page directory. */ asm volatile("movl %0,%%cr3": :"r" (__pa(swapper_pg_dir))); [目录] -------------------------------------------------------------------------------- mlock代码分析 系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。 mlock调用的语法为: int sys_mlock(unsigned long start, size_t len); 初始化为: len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK; start &=PAGE_MASK; 其中mlock又调用do_mlock(),语法为: int do_mlock(unsigned long start, size_t len,int on); 初始化为: len=(len+~PAGE_MASK)&AGE_MASK; 由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK)的内存区域的页进行加锁。 sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。 内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。 内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面 mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下: ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。 EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。 EINVAL:输入参数len不是个合法的正数。 mlock所用到的主要数据结构和重要常量 1.mm_struct struct mm_struct { int count; pgd_t * pgd; /* 进程页目录的起始地址,如图2-3所示 */ unsigned long context; unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack, start_mmap; unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags; struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */ struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */ struct semaphore mmap_sem; } start_code、end_code:进程代码段的起始地址和结束地址。 start_data、end_data:进程数据段的起始地址和结束地址。 arg_start、arg_end:调用参数区的起始地址和结束地址。 env_start、env_end:进程环境区的起始地址和结束地址。 rss:进程内容驻留在物理内存的页面总数。 2. 虚存段(vma)数据结构:vm_area_atruct 虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述: struct vm_area_struct { struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; pgprot_t vm_page_prot; unsigned short vm_flags; /* AVL tree of VM areas per task, sorted by address */ short vm_avl_height; struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right; /* linked list of VM areas per task, sorted by address */ struct vm_area_struct * vm_next; /* for areas with inode, the circular list inode->;i_mmap */ /* for shm areas, the circular list of attaches */ /* otherwise unused */ struct vm_area_struct * vm_next_share; struct vm_area_struct * vm_prev_share; /* more */ struct vm_operations_struct * vm_ops; unsigned long vm_offset; struct inode * vm_inode; unsigned long vm_pte; /* shared mem */ }; vm_start;//所对应内存区域的开始地址 vm_end; //所对应内存区域的结束地址 vm_flags; //进程对所对应内存区域的访问权限 vm_avl_height;//avl树的高度 vm_avl_left; //avl树的左儿子 vm_avl_right; //avl树的右儿子 vm_next;// 进程所使用的按地址排序的vm_area链表指针 vm_ops;//一组对内存的操作 这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。 当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。 属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。 为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。 对平衡树mmap_avl的任何操作必须满足平衡树的一些规则: Consistency and balancing rulesJ(一致性和平衡规则): tree->;vm_avl_height==1+max(heightof(tree->;vm_avl_left),heightof( tree->;vm_avl_right)) abs( heightof(tree->;vm_avl_left) - heightof(tree->;vm_avl_right) ) <= 1 foreach node in tree->;vm_avl_left: node->;vm_avl_key <= tree->;vm_avl_key, foreach node in tree->;vm_avl_right: node->;vm_avl_key >;= tree->;vm_avl_key. 注:其中node->;vm_avl_key= node->;vm_end 对vma可以进行加锁、加保护、共享和动态扩展等操作。 3.重要常量 mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下: PAGE_SHIFT 12 // PAGE_SHIFT determines the page size PAGE_SIZE 0x1000 //1UL<<AGE_SHIFT PAGE_MASK ~(PAGE_SIZE-1) //a very useful constant variable RLIMIT_MEMLOCK 8 //max locked-in-memory address space VM_LOCKED 0x2000 //8*1024=8192, vm_flags的标志之一。 PF_SUPERPRIV 0x00000100 //512, mlock系统调用代码函数功能分析 下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程): suser():如果用户有效(即current->;euid == 0 ),则设置进程标志为root优先权(current->;flags |= PF_SUPERPRIV),并返回1;否则返回0。 find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->;mmap_avl->;vm_start<=addr< mm->;mmap_avl->;vm_end的vma,如果成功则返回此vma;否则返回空null。 mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。 merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。 mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。 mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->;start和new_vma->;end)大小序列把新生成的new->;vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。 注:vma->;vm_offset+= vma->;vm_start-new_vma->;vm_start; mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->;vma插入到当前进程mm的mmap链或mmap_avl树中。 注:new_vma->;vm_offset= vma->;vm_offset+(new_vma->;vm_start-vma->;vm_start); mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->;vma和right->;vma插入到当前进程mm的mmap链或mmap_avl树中。 注:vma->;vm_offset += vma->;vm_start-left_vma->;vm_start; right_vma->;vm_offset += right_vma->;vm_start-left_vma->;vm_start; kmalloc():将在后面3.3中有详细讨论。 insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->;inode的i_mmap环(循环共享链)中。 avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。 avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。 down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。 down定义在/include/linux/sched.h中: extern inline void down(struct semaphore * sem) { if (sem->;count <= 0) __down(sem); sem->;count--; } up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。 up定义在/include/linux/sched.h中: extern inline void up(struct semaphore * sem) { sem->;count++; wake_up(&sem->;wait); } kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。 avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。 avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。 remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->;immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->;immap环中删除的该结点。 [目录] -------------------------------------------------------------------------------- memory.c Memory.c中,Linux提供了对虚拟内存操作的若干函数,其中包括对虚拟页的复制、新建页表、清除页表、处理缺页中断等等。 [目录] -------------------------------------------------------------------------------- copy_page 1.static inline void copy_page(unsigned long from, unsigned long to) 为了节约内存的使用,在系统中,各进程通常采用共享内存,即不同的进程可以共享同一段代码段或数据段。当某一进程发生对共享的内存发生写操作时,为了不影响其它进程的正常运行,系统将把该内存块复制一份,供需要写操作的进程使用,这就是所谓的copy-on-write机制。copy_page就是提供复制内存功能的函数,它调用C语言中标准的内存操作函数,将首地址为from的一块虚拟内存页复制到首地址为to的空间中。 [目录] -------------------------------------------------------------------------------- clear_page_tables 2、void clear_page_tables(struct task_struct * tsk) clear_page_table的功能是将传入的结构tsk中的pgd页表中的所有项都清零,同时将二级页表所占的空间都释放掉。传入clear_page_tables的是当前进程的tsk结构,取得该进程的一级页目录指针pgd后,采用循环的方式,调用free_one_pgd清除pgd表。表共1024项。在free_one_pgd中,实际执行的功能只调用一次free_one_pmd(在80x86中,由于硬件的限制,只有两级地址映射,故将pmd与pgd合并在一起)。在free_one_pmd中,函数调用pte_free将对应于pmd的二级页表所占的物理空间释放掉(进程代码、数据所用的物理内存在do_munmap释放掉了)并将pmd赋值为零。 clear_page_table在系统启动一个可执行文件的映象或载入一个动态链接库时被调用。在fs/exec.c中的do_load_elf_binary()或do_load_aout_binary()调用flash_old_exec,后者调用exec_mmap,而exec_mmap调用clear_page_table。其主要功能是当启动一个新的应用程序的时候,将复制的mm_struct中的页表清除干净,并释放掉原有的所有二级页表空间。 [目录] -------------------------------------------------------------------------------- oom 3、void oom(struct task_struct * task) 返回出错信息。 [目录] -------------------------------------------------------------------------------- free_page_tables 4、void free_page_tables(struct mm_struct * mm) 在free_page_table中,大部分的代码与clear_page_table中的函数一致。所不同的是,该函数在最后调用了pgd_free(page_dir),即不光释放掉二级页表所占的空间,同时还释放一级页目录所占的空间。这是因为free_page_tables被__exit_mm调用,__exit_mm又被do_exit (kernel/kernel.c)调用。当进程中止、系统退出或系统重起时都需要用do_exit(属于进程管理)将所有的进程结束掉。在结束进程过程中 ,将调用free_page_table将进程的空间全部释放掉,当然包括释放进程一级页目录所占的空间。 [目录] -------------------------------------------------------------------------------- new_page_tables 5、int new_page_tables(struct task_struct * tsk) 该函数的主要功能是建立新的页目录表,它的主要流程如如下: ·调用pgd_alloc()为新的页目录表申请一片4K空间 。 ·将初始化进程的内存结构中从768项开始到1023项的内容复制给新的页表(所有的进程都共用虚拟空间中 3G~4G的内存,即在核心态时可以访问所有相同的存储空间)。 ·调用宏SET_PAGE_DIR(include/asm/pgtable.h)将进程控制块tsk->;ts->;CR3的值改为新的页目录表的首地址,同时将CPU中的CR3寄存器的值改为新的页目录表的首地址,从而使新进程进入自己的运行空间。 ·将tsk->;mm->;pgd改为新的页目录表的首地址。 ·new_page_tables被copy_mm调用,而copy_mm被copy_mm_do_fork调用,这两个函数都在kernel/fork.c中。同时,new_page_tables也可以在exec_mmap(fs/exec.c)中调用。即新的进程的产生可以通过两种途径,一种是fork,在程序中动态地生成新的进程,这样新进程的页表原始信息利用copy_mm从其父进程中继承而得,另一种是运行一个可执行文件映象,通过文件系统中的exec.c,将映象复制到tsk结构中。两种方法都需要调用new_page_tables为新进程分配页目录表。 [目录] -------------------------------------------------------------------------------- copy_one_pte 6、static inline void copy_one_pte(pte_t * old_pte, pte_t * new_pte, int cow) 将原pte页表项复制到new_pte上,其流程如下: ·检测old_pte是否在内存中,如不在物理内存中,调用swap_duplicate按old_pte在swap file中的入口地址,将old_pte复制到内存中,同时把old_pte的入口地址赋给new_pte并返回。反之转向3。 获取old_pte对应的物理地址的页号。 ·根据页号判断old_pte是否为系统保留的,如果为系统保留的,这些页为所有的进程在核心态下使用,用户进程没有写的权利,则只需将old_pte指针直接转赋给new_pte后返回。反之则该pte属于普通内存的,则转向4。 ·根据传入的C-O-W标志,为old_pte置写保护标志,如果该页是从swap_cache中得来的,将old_pte页置上“dirty”标志。将old_pte赋值给new_pte。 ·将mem_map结构中关于物理内存使用进程的个数的数值count加1。 [目录] -------------------------------------------------------------------------------- copy_pte_range 7、static inline int copy_pte_range(pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long address, unsigned long size, int cow) 通过循环调用copy_one_pte将从源src_pmd中以地址address开始的长度为size的空间复制给dst_pmd中。如dst_pmd中还未分配地址为address的页表项,则先给三级页表pte表分配4K空间。(每调用一次copy_one_pte复制4K空间。在一次copy_pte_range中最多可复制4M空间)。 [目录] -------------------------------------------------------------------------------- copy_pmd_range 8、static inline int copy_pmd_range(pgd_t *dst_pgd, pgd_t *src_pgd, unsigned long address, unsigned long size, int cow) 通过循环调用copy_pte_range将从源src_pgd中以地址address开始的长度为size的空间复制给dst_pgd中。如dst_pgd中还未分配地址为address的页表项,则在一级(同时也是二级)页表中给对应的pmd分配目录项。 [目录] -------------------------------------------------------------------------------- copy_page_range 9、int copy_page_range(struct mm_struct *dst, struct mm_struct *src, struct vm_area_struct *vma) 该函数的主要功能是将某个任务或进程的vma块复制给另一个任务或进程。其工作机制是循环调用copy_pmd_range,将vma块中的所有虚拟空间复制到对应的虚拟空间中。在做复制之前,必须确保新任务对应的被复制的虚拟空间中必须都为零。copy_page_range按dup_mmap()->;copy_mm()->;do_fork()的顺序被调用(以上三个函数均在kernel/fork.c中)。当进程被创建的时候,需要从父进程处复制所有的虚拟空间,copy_page_range完成的就是这个任务。 [目录] -------------------------------------------------------------------------------- free_pte 9、static inline void free_pte(pte_t page) 虚存页page如在内存中,且不为系统的保留内存,调用free_page将其释放掉(如在系统保留区中,则为全系统共享,故不能删除)。 如page在swap file中,调用swap_free()将其释放。 [目录] -------------------------------------------------------------------------------- forget_pte 10、static inline void forget_pte(pte_t page) 如page不为空,调用free_pte将其释放。 [目录] -------------------------------------------------------------------------------- zap_pte_range 11、static inline void zap_pte_range(pmd_t * pmd, unsigned long address, unsigned long size) zap为zero all pages的缩写。该函数的作用是将在pmd中从虚拟地址address开始,长度为size的内存块通过循环调用pte_clear将其页表项清零,调用free_pte将所含空间中的物理内存或交换空间中的虚存页释放掉。在释放之前,必须检查从address开始长度为size的内存块有无越过PMD_SIZE.(溢出则可使指针逃出0~1023的区间)。 [目录] -------------------------------------------------------------------------------- zap_pmd_range 12、static inline void zap_p |
网络 BSD是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。两个通信进程都用一个套接口来描述通信链路的两端。套接口可以认为是一种特殊的管道,但和管道不同的是,套接口对于可以容纳的数据的大小没有限制。 Linux支持多种类型的套接口,也叫做套接口寻址族,这是因为每种类型的套接口都有自己的寻址方法。Linux支持以下的套接口类型: UNIX UNIX域套接口 INET Internet地址族TCP/IP协议支持通信。 AX25 Amateur radio X25 IPX Novell IPX APPLETALK Appletalk DDP X25 X25 这些类型的套接口代表各种不同的连接服务。 Linux的BSD 套接口支持下面的几种套接口类型: 1. 流式(stream) 提供了可靠的双向顺序数据流连接。可以保证数据传输中的完整性、正确性和单一性。INET寻址族中TCP协议支持这种类型。 2. 数据报(Datagram) 这种类型的套接口也可以像流式套接口一样提供双向的数据传输,但它们不能保证传输的数据一定能够到达目的节点。即使数据能够到达,也无法保证数据以正确的顺序到达以及数据的单一性、正确性。UDP协议支持这种类型的套接口。 3. 原始(Raw) 这种类型的套接口允许进程直接存取下层的协议。 4. 可靠递送消息(Reliable Delivered Messages) 这种套接口和数据报套接口一样,只能保证数据的到达。 5. 顺序数据包(Sequenced Packets) 这种套接口和流式套接口相同,除了数据包的大小是固定的。 6. 数据包(Packet) 这不是标准的BSD 套接口类型,而是Linux 中的一种扩展。它允许进程直接存取设备层的数据包。 INET套接口层包括支持TCP/IP协议的Internet地址族。正如上面提到的,这些协议是分层的,每一个协议都使用另一个协议的服务。Linux系统中的TCP/IP代码和数据结构也反映了这种分层的思想。它和BSD 套接口层的接口是通过一系列与Internet地址族有关的套接口操作来实现的,而这些套接口操作是在网络初始化的过程中由INET 套接口层在BSD 套接口层中注册的。这些操作和其他地址族的操作一样保存在pops向量中。 BSD 套接口层通过INET的proto_ops数据结构来调用与INET 层有关的套接口子程序来实现有关INET层的服务。例如,当BSD 套接口创建一个发送给INET地址族的请求时将会使用INET的套接口创建功能。BSD 套接口层将会把套接口数据结构传递给每一个操作中的INET层。INET 套接口层在它自己的数据结构sock中而不是在BSD 套接口的数据结构中插入有关TCP/IP的信息,但sock数据结构是和B S D套接口的数据结构有关的。它使用BSD 套接口中的数据指针来连接sock数据结构和BSD 套接口数据结构,这意味着以后的INET 套接口调用可以十分方便地得到sock数据结构。数据结构sock中的协议操作指针也会在创建时设置好,并且此指针是和所需要的协议有关的。如果需要的是TCP协议,那么数据结构sock中的协议操 下面是Linux系统的TCP包,从netif_rx开始 [net/core/dev.c] 中断管理管理: "netif_rx" |netif_rx |__skb_queue_tail |qlen++ |* simple pointer insertion * |cpu_raise_softirq |softirq_active(cpu) |= (1 << NET_RX_SOFTIRQ) // set bit NET_RX_SOFTIRQ in the BH vector ·__skb_queue_tail [include/linux/skbuff.h] ·cpu_raise_softirq [kernel/softirq.c] 中断的后半部分: "net_rx_action" IRQ的基本处理以后,还需要另外的“底半”处理,(参考软中断)这里的是NET_RX_SOFTIRQ完成的。 net_rx_action [net/core/dev.c] net_dev_init [net/core/dev.c] |net_rx_action |skb = __skb_dequeue (the exact opposite of __skb_queue_tail) |for (ptype = first_protocol; ptype < max_protocol; ptype++) // Determine |if (skb->;protocol == ptype) // what is the network protocol |ptype->;func ->; ip_rcv // according to ''struct ip_packet_type [net/ipv4/ip_output.c]'' **** NOW WE KNOW THAT PACKET IS IP **** |ip_rcv |NF_HOOK (ip_rcv_finish) |ip_route_input // search from routing table to determine function to call |skb->;dst->;input ->; ip_local_deliver // according to previous routing table check, destination is local machine |ip_defrag // reassembles IP fragments |NF_HOOK (ip_local_deliver_finish) |ipprot->;handler ->; tcp_v4_rcv // according to ''tcp_protocol [include/net/protocol.c]'' **** NOW WE KNOW THAT PACKET IS TCP **** |tcp_v4_rcv |sk = __tcp_v4_lookup |tcp_v4_do_rcv |switch(sk->;state) *** Packet can be sent to the task which uses relative socket *** |case TCP_ESTABLISHED: |tcp_rcv_established |__skb_queue_tail // enqueue packet to socket |sk->;data_ready ->; sock_def_readable |wake_up_interruptible *** Packet has still to be handshaked by 3-way TCP handshake *** |case TCP_LISTEN: |tcp_v4_hnd_req |tcp_v4_search_req |tcp_check_req |syn_recv_sock ->; tcp_v4_syn_recv_sock |__tcp_v4_lookup_established |tcp_rcv_state_process *** 3-Way TCP Handshake *** |switch(sk->;state) |case TCP_LISTEN: // We received SYN |conn_request ->; tcp_v4_conn_request |tcp_v4_send_synack // Send SYN + ACK |tcp_v4_synq_add // set SYN state |case TCP_SYN_SENT: // we received SYN + ACK |tcp_rcv_synsent_state_process tcp_set_state(TCP_ESTABLISHED) |tcp_send_ack |tcp_transmit_skb |queue_xmit ->; ip_queue_xmit |ip_queue_xmit2 |skb->;dst->;output |case TCP_SYN_RECV: // We received ACK |if (ACK) |tcp_set_state(TCP_ESTABLISHED) ·net_rx_action [net/core/dev.c] ·__skb_dequeue [include/linux/skbuff.h] ·ip_rcv [net/ipv4/ip_input.c] ·NF_HOOK ->; nf_hook_slow [net/core/netfilter.c] ·ip_rcv_finish [net/ipv4/ip_input.c] ·ip_route_input [net/ipv4/route.c] ·ip_local_deliver [net/ipv4/ip_input.c] ·ip_defrag [net/ipv4/ip_fragment.c] ·ip_local_deliver_finish [net/ipv4/ip_input.c] ·tcp_v4_rcv [net/ipv4/tcp_ipv4.c] ·__tcp_v4_lookup ·tcp_v4_do_rcv ·tcp_rcv_established [net/ipv4/tcp_input.c] ·__skb_queue_tail [include/linux/skbuff.h] ·sock_def_readable [net/core/sock.c] ·wake_up_interruptible [include/linux/sched.h] ·tcp_v4_hnd_req [net/ipv4/tcp_ipv4.c] ·tcp_v4_search_req ·tcp_check_req ·tcp_v4_syn_recv_sock ·__tcp_v4_lookup_established ·tcp_rcv_state_process [net/ipv4/tcp_input.c] ·tcp_v4_conn_request [net/ipv4/tcp_ipv4.c] ·tcp_v4_send_synack ·tcp_v4_synq_add ·tcp_rcv_synsent_state_process [net/ipv4/tcp_input.c] ·tcp_set_state [include/net/tcp.h] ·tcp_send_ack [net/ipv4/tcp_output.c] [目录] -------------------------------------------------------------------------------- 网络接口源码导读 [目录] -------------------------------------------------------------------------------- 网络接口源码的结构(一) Linux最新稳定内核2.4.x的网络接口源码的结构(一) 李元佳 一.前言 Linux的源码里,网络接口的实现部份是非常值得一读的,通过读源码,不仅对网络协议会有更深的了解,也有助于在网络编程的时候,对应用函数有更精确的了解和把握。本文把重点放在网络接口程序的总体结构上,希望能作为读源码时一些指导性的文字。 本文以Linux2.4.16内核作为讲解的对象,内核源码可以在http://www.kernel.org上下载。我读源码时参考的是http://lxr.linux.no/这个交差参考的网站,我个人认为是一个很好的工具,如果有条件最好上这个网站。国内http://211.71.69.201/joyfire/有类似 二.网络接口程序的结构 Linux的网络接口分为四部份:网络设备接口部份,网络接口核心部份,网络协议族部份,以及网络接口socket层。 网络设备接口部份主要负责从物理介质接收和发送数据。实现的文件在linu/driver/net目录下面。 网络接口核心部份是整个网络接口的关键部位,它为网络协议提供统一的发送接口,屏蔽各种各样的物理介质,同时有负责把来自下层的包向合适的协议配送。它是网络接口的中枢部份。它的主要实现文件在linux/net/core目录下,其中linux/net/core/dev.c为主要管理文件。 网络协议族部份是各种具体协议实现的部份。Linux支持TCP/IP,IPX,X.25,AppleTalk等的协议,各种具体协议实现的源码在linux/net/目录下相应的名称。在这里主要讨论TCP/IP(IPv4)协议,实现的源码在linux/net/ipv4,其中linux/net/ipv4/af_inet.c是主要的管理文件。 网络接口Socket层为用户提供的网络服务的编程接口。主要的源码在linux/net/socket.c 三.网络设备接口部份 物理层上有许多不同类型的网络接口设备, 在文件include/linux/if_arp.h的28行里定义了ARP能处理的各种的物理设备的标志符。网络设备接口要负责具体物理介质的控制,从物理介质接收以及发送数据,并对物理介质进行诸如最大数据包之类的各种设置。这里我们以比较简单的3Com3c501 太网卡的驱动程序为例,大概讲一下这层的工作原理。源码在Linux/drivers/net/3c501.c。 我们从直觉上来考虑,一个网卡当然最主要的是完成数据的接收和发送,在这里我们来看看接收和发送的过程是怎么样的。 发送相对来说比较简单,在Linux/drivers/net/3c501.c的行475 开始的el_start_xmit()这个函数就是实际向3Com3c501以太网卡发送数据的函数,具体的发送工作不外乎是对一些寄存器的读写,源码的注释很清楚,大家可以看看。 接收的工作相对来说比较复杂。通常来说,一个新的包到了,或者一个包发送完成了,都会产生一个中断。Linux/drivers/net/3c501.c的572开始el_interrupt()的函数里面,前半部份处理的是包发送完以后的汇报,后半部份处理的是一个新的包来的,就是说接收到了新的数据。el_interrupt()函数并没有对新的包进行太多的处理,就交给了接收处理函数el_receive()。el_receive()首先检查接收的包是否正确,如果是一个“好”包就会为包分配一个缓冲结构(dev_alloc_skb()),这样驱动程序对包的接收工作就完成了,通过调用上层的函数netif_rx()(net/core/dev.c1214行) ,把包交给上层。 现在驱动程序有了发送和接收数据的功能了,驱动程序怎么样和上层建立联系呢?就是说接收到包以后怎么送给上层,以及上层怎么能调用驱动程序的发送函数呢? 由下往上的关系,是通过驱动程序调用上层的netif_rx()(net/core/dev.c 1214行)函数实现的,驱动程序通过这个函数把接到的数据交给上层,请注意所有的网卡驱动程序都需要调用这个函数的,这是网络接口核心层和网络接口设备联系的桥梁。 由上往下的关系就复杂点。网络接口核心层需要知道有多少网络设备可以用,每个设备的函数的入口地址等都要知道。网络接口核心层会大声喊,“嘿,有多少设备可以帮我发送数据包?能发送的请给我排成一队!”。这一队就由dev_base开始,指针structnet_device *dev_base (Linux/include/linux/netdevice.h 436行)就是保存了网络接口核心层所知道的所有设备。对于网络接口核心层来说,所有的设备都是一个net_device结构,它在include/linux/netdevice.h,line 233里被定义,这是从网络接口核心层的角度看到的一个抽象的设备,我们来看看网络接口核心层的角度看到的网络设备具有的功能: struct net_device { ……… open() stop() hard_start_xmit() hard_header() rebuild_header() set_mac_address() do_ioctl() set_config() hard_header_cache() header_cache_update() change_mtu() tx_timeout() hard_header_parse() neigh_setup() accept_fastpath() ……… } 如果网络接口核心层需要由下层发送数据的时候,在dev_base找到设备以后,就直接调dev->;hard_start_xmit()的这个函数来让下层发数据包。 驱动程序要让网络接口核心层知道自己的存在,当然要加入dev_base所指向的指针链,然后把自己的函数以及各种参数和net_device里的相应的域对应起来。加入dev_base所指向的指针链是通过函数register_netdev(&dev_3c50)(linux/drivers/net/net_init.c, line 532) 建立的。而把自己的函数以和net_device里的相应的域及各种参数关系的建立是在el1_probe1()(Linux/drivers/net/3c501.c)里进行的: el1_probe1(){ ……… dev->;open = &el_open; dev->;hard_start_xmit = &el_start_xmit; dev->;tx_timeout = &el_timeout; dev->;watchdog_timeo = HZ; dev->;stop = &el1_close; dev->;get_stats = &el1_get_stats; dev->;set_multicast_list = &set_multicast_list; ……… ether_setup(dev); ……… } 进一步的对应工作在ether_setup(dev) (drivers/net/net_init.c, line 405 )里进行。我们注意到dev->;hard_start_xmit =&el_start_xmit,这样发送函数的关系就建立了,上层只知道调用dev->;hard_start_xmit这个来发送数据,上面的语句就把驱动程序实际的发送函数告诉了上层。 四.网络接口核心部分 刚才谈论了驱动程序怎么和网络接口核心层衔接的。网络接口核心层知道驱动程序以及驱动程序的函数的入口是通过*dev_base指向的设备链的,而下层是通过调用这一层的函数netif_rx()(net/core/dev.c 1214行) 把数据传递个这一层的。 网络接口核心层的上层是具体的网络协议,下层是驱动程序,我们以及解决了下层的关系,但和上层的关系没有解决。先来讨论一下网络接口核心层和网络协议族部份的关系,这种关系不外乎也是接收和发送的关系。 网络协议,例如IP,ARP等的协议要发送数据包的时候会把数据包传递给这层,那么这种传递是通过什么函数来发生的呢?网络接口核心层通过dev_queue_xmit()(net/core/dev.c,line975)这个函数向上层提供统一的发送接口,也就是说无论是IP,还是ARP协议,通过这个函数把要发送的数据传递给这一层,想发送数据的时候就调用这个函数就可以了。dev_queue_xmit()做的工作最后会落实到dev->;hard_start_xmit(),而dev->;h ard_start_xmit()会调用实际的驱动程序来完成发送的任务。例如上面的例子中,调用dev->;hard_start_xmit()实际就是调用了el_start_xmit()。 现在讨论接收的情况。网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,这时候当然要把数据包往上层派送。所有的协议族的下层协议都需要接收数据,TCP/IP的IP协议和ARP协议,SPX/IPX的IPX协议,AppleTalk的DDP和AARP协议等都需要直接从网络接口核心层接收数据,网络接口核心层接收数据是如何把包发给这些协议的呢?这时的情形和于下层的关系很相似,网络接口核心层的下面 可能有许多的网卡的驱动程序,为了知道怎么向这些驱动程序发数据,前面以及讲过时,是通过*dev_base这个指针指向的链解决的,现在解决和上层的关系是通过static struct packet_ptype_base[16]( net/core/dev.c line 164)这个数组解决的。这个数组包含了需要接收数据包的协议,以及它们的接收函数的入口。 从上面可以看到,IP协议接收数据是通过ip_rcv()函数的,而ARP协议是通过arp_rcv()的,网络接口核心层只要通过这个数组就可以把数据交给上层函数了。 如果有协议想把自己添加到这个数组,是通过dev_add_pack()(net/core/dev.c, line233)函数,从数组删除是通过dev_remove_pack()函数的。Ip层的注册是在初始化函数进行的 void __init ip_init(void) (net/ipv4/ip_output.c, line 1003) { ……… dev_add_pack(&ip_packet_type); ……… } 重新到回我们关于接收的讨论,网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,看看这个函数做了些什么。 由于现在还是在中断的服务里面,所有并不能够处理太多的东西,剩下的东西就通过cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ) 交给软中断处理, 从open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL)可以知道NET_RX_SOFTIRQ软中断的处理函数是net_rx_action()(net/core/dev.c, line 1419),net_rx_action()根据数据包的协议类型在数组ptype_base[16]里找到相应的协议,并从中知道了接收的处理函数,然后把数据包交给处理函数,这样就交给了上层处理,实际调用处理函数是通过net_rx_action()里的pt_prev->;func()这一句。例如如果数据 包是IP协议的话,ptype_base[ETH_P_IP]->;func()(ip_rcv()),这样就把数据包交给了IP协议。 五.网络协议部分 协议层是真正实现是在这一层。在linux/include/linux/socket.h里面,Linux的BSD Socket定义了多至32支持的协议族,其中PF_INET就是我们最熟悉的TCP/IP协议族(IPv4, 以下没有特别声明都指IPv4)。以这个协议族为例,看看这层是怎么工作的。实现TCP/IP协议族的主要文件在inux/net/ipv4/目录下面,Linux/net/ipv4/af_inet.c为主要的管理文件。 在Linux2.4.16里面,实现了TCP/IP协议族里面的的IGMP,TCP,UDP,ICMP,ARP,IP。我们先讨论一下这些协议之间的关系。IP和ARP协议是需要直接和网络设备接口打交道的协议,也就是需要从网络核心模块(core) 接收数据和发送数据的。而其它协议TCP,UDP,IGMP,ICMP是需要直接利用IP协议的,需要从IP协议接收数据,以及利用IP协议发送数据,同时还要向上层Socket层提供直接的调用接口。可以看到IP层是一个核心的协议,向 下需要和下层打交道,又要向上层提供所以的传输和接收的服务。 先来看看IP协议层。网络核心模块(core) 如果接收到IP层的数据,通过ptype_base[ETH_P_IP] 数组的IP层的项指向的IP协议的ip_packet_type->;ip_rcv()函数把数据包传递给IP层,也就是说IP层通过这个函数ip_rcv()(linux/net/ipv4/ip_input.c)接收数据的。ip_rcv()这个函数只对IP数据保做了一些checksum的检查工作,如果包是正确的就把包交给了下一个处理函数ip_rcv_finish()(注意调用是通过NF_HOOK这个宏实现的)。现在,ip_rcv_finish()这个函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由,要决定把数据包往那里送。路由的工作是通过函数ip_route_input()(/linux/net/ipv4/route.c,line 1622)实现的。对于进来的包可能的路由有这些: 属于本地的数据(即是需要传递给TCP,UDP,IGMP这些上层协议的) ; 需要要转发的数据包(网关或者NAT服务器之类的); 不可能路由的数据包(地址信息有误); 我们现在关心的是如果数据是本地数据的时候怎么处理。ip_route_input()调用ip_route_input_slow()(net/ipv4/route.c, line 1312),在ip_route_input_slow()里面的1559行rth->;u.dst.input= ip_local_deliver,这就是判断到IP包是本地的数据包,并把本地数据包处理函数的地址返回。好了,路由工作完成了,返回到ip_rcv_finish()。ip_rcv_finish()最后调用拉skb->;dst->;input(skb),从上面可以看到,这其实就是调用了ip_local_deliver()函数,而ip_local_deliver(),接着就调用了ip_local_deliver_finish()。现在真正到了往上层传递数据包的时候了。 现在的情形和网络核心模块层(core) 往上层传递数据包的情形非常相似,怎么从多个协议选择合适的协议,并且往这个协议传递数据呢?网络网络核心模块层(core) 通过一个数组ptype_base[16]保存了注册了的所有可以接收数据的协议,同样网络协议层也定义了这样一个数组struct net_protocol*inet_protos[MAX_INET_PROTOS](/linux/net/ipv4/protocol.c#L102),它保存了所有需要从IP协议层接收数据的上层协议(IGMP,TCP,UDP,ICMP)的接收处理函数的地址。我们来看看TCP协议的数据结构是怎么样的: linux/net/ipv4/protocol.c line67 static struct inet_protocol tcp_protocol = { handler: tcp_v4_rcv,// 接收数据的函数 err_handler: tcp_v4_err,// 出错处理的函数 next: IPPROTO_PREVIOUS, protocol: IPPROTO_TCP, name: "TCP" }; 第一项就是我们最关心的了,IP层可以通过这个函数把数据包往TCP层传的。在linux/net/ipv4/protocol.c的上部,我们可以看到其它协议层的处理函数是igmp_rcv(),udp_rcv(), icmp_rcv()。同样在linux/net/ipv4/protocol.c,往数组inet_protos[MAX_INET_PROTOS] 里面添加协议是通过函数inet_add_protocol()实现的,删除协议是通过 inet_del_protocol()实现的。inet_protos[MAX_INET_PROTOS]初始化的过程在linux/net/ipv4/af_inet.c inet_init()初始化函数里面。 inet_init(){ …… printk(KERN_INFO "IP Protocols: "; for (p = inet_protocol_base; p != NULL { struct inet_protocol *tmp = (struct inet_protocol *) p->;next; inet_add_protocol(p);// 添加协议 printk("%s%s",p->;name,tmp?", ":"n"; p = tmp; ……… } [目录] -------------------------------------------------------------------------------- 网络接口源码的结构(二) Linux最新稳定内核2.4.x的网络接口源码的结构(二) 李元佳 如果你在Linux启动的时候有留意启动的信息, 或者在linux下打命令dmesg就可以看到这一段程序输出的信息: IP Protocols: ICMP,UDP,TCP,IGMP也就是说现在数组inet_protos[]里面有了ICMP,UDP,TCP,IGMP四个协议的inet_protocol数据结构,数据结构包含了它们接收数据的处理函数。 Linux 2.4.16在linux/include/linux/socket.h里定义了32种支持的BSDsocket协议,常见的有TCP/IP,IPX/SPX,X.25等,而每种协议还提供不同的服务,例如TCP/IP协议通过TCP协议支持连接服务,而通过UDP协议支持无连接服务,面对这么多的协议,向用户提供统一的接口是必要的,这种统一是通过socket来进行的。 在BSD socket网络编程的模式下,利用一系列的统一的函数来利用通信的服务。例如一个典型的利用TCP协议通信程序是这样: sock_descriptor = socket(AF_INET,SOCK_STREAM,0); connect(sock_descriptor, 地址,) ; send(sock_descriptor,”hello world”); recv(sock_descriptor,buffer,1024,0); 第一个函数指定了协议Inet协议,即TCP/IP协议,同时是利用面向连接的服务,这样就对应到TCP协议,以后的操作就是利用socket的标准函数进行的。 从上面我们可以看到两个问题,首先socket层需要根据用户指定的协议族(上面是AF_INET) 从下面32种协议中选择一种协议来完成用户的要求,当协议族确定以后,还要把特定的服务映射到协议族下的具体协议,例如当用户指定的是面向连接的服务时,Inet协议族会映射到TCP协议。 从多个协议中选择用户指定的协议,并把具体的出理交给选中的协议,这和一起网络核心层向上和向下衔接的问题本质上是一样的,所以解决的方法也是一样的,同样还是通过数组。在Linux/net/socket.c定义了这个数组staticstruct net_proto_family*net_families[NPROTO] 。数组的元素已经确定了,net_families[2]是TCP/IP协议,net_families[3]是X.25协议,具体那一项对应什么协议,在include/linux/socket.h有定义。但是每一项的数据结构net_proto_family的ops是空的,也就是具体协议处理函数的地址是不知道的。协议的处理函数和ops建立联系是通过sock_register()(Linux/net/socket.c)这个函数建立的,例如TCP/IP协议的是这样建立关系的: int __init inet_init(void) (net/ipv4/af_inet.c) { (void) sock_register(&inet_family_ops); } 只要给出AF_INET(在宏里定义是2),就可以找到net_failies[2] 里面的处理函数了。 协议的映射完成了,现在要进行服务的映射了。上层当然不可能知道下层的什么协议能对应特定的服务,所以这种映射自然由协议族自己完成。在TCP/IP协议族里,这种映射是通过struct list_head inetsw[SOCK_MAX]( net/ipv4/af_inet.c) 这个数组进行映射的,在谈论这个数组之前我们来看另外一个数组inetsw_array[](net/ipv4/af_inet.c) static struct inet_protosw inetsw_array[] = { { type: SOCK_STREAM, protocol: IPPROTO_TCP, prot: &tcp_prot, ops: &inet_stream_ops, capability: -1, no_check: 0, flags: INET_PROTOSW_PERMANENT, }, { type: SOCK_DGRAM, protocol: IPPROTO_UDP, prot: &udp_prot, ops: &inet_dgram_ops, capability: -1, no_check: UDP_CSUM_DEFAULT, flags: INET_PROTOSW_PERMANENT, }, { type: SOCK_RAW, protocol: IPPROTO_IP, /* wild card */ prot: &raw_prot, ops: &inet_dgram_ops, capability: CAP_NET_RAW, no_check: UDP_CSUM_DEFAULT, flags: INET_PROTOSW_REUSE, } }; 我们看到,SOCK_STREAM映射到了TCP协议,SOCK_DGRAM映射到了UDP协议,SOCK_RAW映射到了IP协议。现在只要把inetsw_array里的三项添加到数组inetsw[SOCK_MAX]就可以了,添加是通过函数inet_register_protosw()实现的。在inet_init()(net/ipv4/af_inet.c) 里完成了这些工作。 还有一个需要映射的就是socket其它诸如accept,send(), connect(),release(),bind()等的操作函数是怎么映射的呢?我们来看一下上面的数组的TCP的项 { type: SOCK_STREAM, protocol: IPPROTO_TCP, prot: &tcp_prot, ops: &inet_stream_ops, capability: -1, no_check: 0, flags: INET_PROTOSW_PERMANENT, }, 我们看到这种映射是通过ops,和prot来映射的,我们再来看看 tcp_prot这一项: struct proto tcp_prot = { name: "TCP", close: tcp_close, connect: tcp_v4_connect, disconnect: tcp_disconnect, accept: tcp_accept, ioctl: tcp_ioctl, init: tcp_v4_init_sock, destroy: tcp_v4_destroy_sock, shutdown: tcp_shutdown, setsockopt: tcp_setsockopt, getsockopt: tcp_getsockopt, sendmsg: tcp_sendmsg, recvmsg: tcp_recvmsg, backlog_rcv: tcp_v4_do_rcv, hash: tcp_v4_hash, unhash: tcp_unhash, get_port: tcp_v4_get_port, }; 所以的映射都已经完成了,用户调用connect()函数,其实就是调用了tcp_v4_connect()函数,按照这幅图,读起源码来就简单了很多了。 六 Socket层 上一节把socket层大多数要讨论的东西都谈论了,现在只讲讲socket 层和用户的衔接。 系统调用socket(),bind(),connect(),accept,send(),release()等是在Linux/net/socket.c里面的实现的,系统调用实现的函数是相应的函数名加上sys_的前缀。 现在看看当用户调用socket()这个函数,到底下面发生了什么。 Socket(AF_INET,SOCK_STREAM,0)调用了sys_socket(),sys_socket()接着调用socket_creat(),socket_creat()就要根据用户提供的协议族参数在net_families[]里寻找合适的协议族,如果协议族没有被安装就要请求安装该协议族的模块,然后就调用该协议族的create()函数的处理句柄。根据参数AF_INET,inet_creat()就被调用了,在inet_creat()根据服务类型在inetsw[SOCK_MAX]选择合适的协议,并把协议的操作集赋给socket就是了,根据SOCK_STREAM,TCP协议被选中, inet_creat(){ answer=inetsw [用户要求服务服务] ; sock->;ops = answer->;ops; sk->;prot = answer->;prot } 到此为止,上下都打通了,该是大家读源码的时候了。 [目录] -------------------------------------------------------------------------------- 防火墙 [目录] -------------------------------------------------------------------------------- netfilter 标题: A new place to LKM:netfilter 作者: yawl <yawl@nsfocus.com>; 时间: 2000-10 目录: -.前言 二.分析 三.例子代码 四.附录:与2.2在应用方面的区别简介 五.后记 -.前言 在linux2.2内核中的防火墙ipchains已经被用户广泛认可,它提供了完整的防火墙功能(包过滤,地址伪装,透明代理),又避免了商业防火墙那高的惊人的价格。如果你用的是某款国产防火墙,那么十有八九你实际在受到ipchains(有些甚至是2.0系列中ipfwadm)的保护.在未来的2.4内核中,被称为netfilter(http://netfilter.kernelnotes.org/)的防火墙以更好的结构重新构造,并实现了许多新功能,如完整的动态NAT(2.2内核实际是多对一的"地址伪装",基于MAC及用户的过滤,真正的基于状态的过滤(不再是简单的查看tcp的标志位等),包速率限制等。 在原有的网络部分的LKM中,如果对网络部分进行处理,一般是先生成struct packet_type结构,在用dev_add_pack将其插入网络层(注意此时的packet_type实际相当于一个的三层的协议,如ip_packet_type,ipx_8023_packet_type等),具体的例子可参见phrack 55期<Building into the linux network layer>;和本月小四写的月刊文章<利用LLKM处理网络通信----对抗IDS、Firewall>;。 而netfilter本身在IP层内提供了另外的5个插入点(其文档中称为HOOK):NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING,分别对应IP层的五个不同位置,这样理论上在写lkm时便可以选择更适合的切入点,再辅以netfilter内置的新功能(如connect tracking),应该会帮助写出功能更强的lkm。 本来准备写出一个完整的例子(限制IP连接数),但计划总赶不上变化,只好先贴出个简单的例子来,权且自我安慰成抛砖引玉了。 本文的参考配置是linux2.4.0-test4和iptable-1.1.1,好,开始抛砖,闪人喽! 二.分析 通俗的说,netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上上登记了一些处理函数进行处理(如包过滤,NAT等,甚至可以是用户自定义的功能)。 IP层的五个HOOK点的位置如下图所示(copy from <packet filter howto>;) : --->;[1]--->;[ROUTE]--->;[3]--->;[5]--->; | ^ | | | [ROUTE] v | [2] [4] | ^ | | v | [local process] [1]:NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测),源地址转换在此点 进行; [2]:NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行; [3]:NF_IP_FORWARD:要转发的包通过此检测点,FORWORD包过滤在此点进行; [4]:NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行; [5]:NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,内置的目的地址转换功能(包括地址伪 装)在此点进行。 在IP层代码中,有一些带有NF_HOOK宏的语句,如IP的转发函数中有: <-ipforward.c ip_forward()->; NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->;dev, dev2, ip_forward_finish); 其中NF_HOOK宏的定义提炼如下: <-/include/linux/netfilter.h->; #ifdef CONFIG_NETFILTER #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \ (list_empty(&nf_hooks[(pf)][(hook)]) \ ? (okfn)(skb) \ : nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn))) #else /* !CONFIG_NETFILTER */ #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb) #endif /*CONFIG_NETFILTER*/ 如果在编译内核时没有配置netfilter时,就相当于调用最后一个参数,此例中即执行ip_forward_finish函数;否则进入HOOK点,执行通过nf_register_hook()登记的功能(这句话表达的可能比较含糊,实际是进入 nf_hook_slow()函数,再由它执行登记的函数)。 NF_HOOK宏的参数分别为: 1.pf:协议族名,netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。 2.hook:HOOK点的名字,对于IP层,就是取上面的五个值; 3.skb:不用多解释了吧; 4.indev:进来的设备,以struct net_device结构表示; 5.outdev:出去的设备,以struct net_device结构表示; (后面可以看到,以上五个参数将传到用nf_register_hook登记的处理函数中。) 6.okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。 这些点是已经在内核中定义好的,除非你是这部分内核代码的维护者,否则无权增加或修改,而在此检测点进行的处理,则可由用户指定。像packet filter,NAT,connection track这些功能,也是以这种方式提供的。正如netfilter的当初的设计目标--提供一个完善灵活的框架,为扩展功能提供方便。 如果我们想加入自己的代码,便要用nf_register_hook函数,其函数原型为: int nf_register_hook(struct nf_hook_ops *reg) 我们考察一下struct nf_hook_ops结构: struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; int pf; int hooknum; /* Hooks are ordered in ascending priority. */ int priority; }; 我们的工作便是生成一个struct nf_hook_ops结构的实例,并用nf_register_hook将其HOOK上。其中list项我们总要初始化为{NULL,NULL};由于一般在IP层工作,pf总是PF_INET;hooknum就是我们选择的HOOK点;一个HOOK点可能挂多个处理函数,谁先谁后,便要看优先级,即priority的指定了。netfilter_ipv4.h中用一个枚举类型指定了内置的处理函数的优先级: enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_LAST = INT_MAX, }; hook是提供的处理函数,也就是我们的主要工作,其原型为: unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)); 它的五个参数将由NFHOOK宏传进去。 了解了这些,基本上便可以可以写一个lkm出来了。 三.例子代码 这段代码是一个例子,其功能实现了一个IDS,检测几个简单攻击(land,winnuke)和特殊扫描(nmap),当然,不会有人真把 它当严肃的IDS使用吧。可以利用类似结构干点别的。。。 <-example.c begin->; /* * netfilter module example: it`s a kernel IDS(be quie,donot laugh, my friend) * yawl@nsfocus.com * Compile:gcc -O -c -Wall sample.c ,under linux2.4 kernel,netfilter is needed. */ #define __KERNEL__ #define MODULE #include <linux/module.h>; #include <linux/skbuff.h>; #include <linux/netdevice.h>; #include <linux/config.h>; #include <linux/ip.h>; #include <linux/tcp.h>; #include <linux/udp.h>; #include <linux/netfilter_ipv4.h>; #define ALERT(fmt,args...) printk("nsfocus: " fmt, ##args) /*message will be print to screen(too many~),and logged to /var/log/message*/ static unsigned int sample(unsigned int hooknum,struct sk_buff **skb, const struct net_device *in, const struct net_device *out,int (*okfn)(struct sk_buff *)) { struct iphdr *iph; struct tcphdr *tcph; struct udphdr *udph; __u32 sip; __u32 dip; __u16 sport; __u16 dport; iph=(*skb)->;nh.iph; sip=iph->;saddr; dip=iph->;daddr; /*play ip packet here (note:checksum has been checked,if connection track is enabled,defrag have been done )*/ if(iph->;ihl!=5){ ALERT("IP packet with packet from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if(iph->;protocol==6){ tcph=(struct tcphdr*)((__u32 *)iph+iph->;ihl); sport=tcph->;source; dport=tcph->;dest; /*play tcp packet here*/ if((tcph->;syn)&&(sport==dport)&&(sip==dip)){ ALERT("maybe land attack\n"; } if(ntohs(tcph->;dest)==139&&tcph->;urg){ ALERT("maybe winnuke a from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if(tcph->;ece&&tcph->;cwr){ ALERT("queso from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if((tcph->;fin)&&(tcph->;syn)&&(!tcph->;rst)&&(!tcph->;psh)&&(!tcph->;ack)&&(!tcph->;urg)){ ALERT("SF_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if((!tcph->;fin)&&(!tcph->;syn)&&(!tcph->;rst)&&(!tcph->;psh)&&(!tcph->;ack)&&(!tcph->;urg)){ ALERT("NULL_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if(tcph->;fin&&tcph->;syn&&tcph->;rst&&tcph->;psh&&tcph->;ack&&tcph->;urg){ ALERT("FULL_Xmas_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } if((tcph->;fin)&&(!tcph->;syn)&&(!tcph->;rst)&&(tcph->;psh)&&(!tcph->;ack)&&(tcph->;urg)){ ALERT("XMAS_Scan(FPU)from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); } } else if(iph->;protocol==17){ udph=(struct udphdr *)((__u32 *)iph+iph->;ihl); sport=udph->;source; dport=udph->;dest; /*play udp packet here*/ } else if(iph->;protocol==1){ /*play icmp packet here*/ } else if(iph->;protocol==2){ ALERT("igmp packet from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip)); /*play igmp packet here*/ } else{ ALERT("unknown protocol%d packet from %d.%d.%d.%d to %d.%d.%d.%d\n",iph->;protocol,NIPQUAD(sip),NIPQUAD(dip)); } return NF_ACCEPT; /*for it is IDS,we just accept all packet, if you really want to drop this skb,just return NF_DROP*/ } static struct nf_hook_ops iplimitfilter ={ {NULL,NULL} ,sample,PF_INET,NF_IP_PRE_ROUTING,NF_IP_PRI_FILTER-1}; int init_module(void) { return nf_register_hook(&iplimitfilter); } void cleanup_module(void) { nf_unregister_hook(&iplimitfilter); } <-example.c end->; 四.附录:与2.2在应用方面的区别简介 本来还想详细介绍一下iptables的用法,但如果说的太详细的话,还不如索性将HOWTO翻译一下,于是干脆了却了这个念头,只顺便简介一下与以前版本的变化(而且是我认为最重要的)。如果ipchains本来便没有在你的脑子中扎根,其实便没有必要看这部分。 netfilter,又可称为iptables.开发初期准备将packet filter和NAT的配置工具完全分开,一个称为iptables,另一个称为ipnatctl,而将整个项目成为netfilter.但后来可能是还是习惯2.2内核中用ipchians一个工具干两件事的用法,又改为全部用iptables配置了。 理论上还可以用2.2系列的ipchains和2.0系列的ipfwadm作配置工具,但只是做兼容或过渡的考虑了。通过源码看到他们也是通过现有的结构HOOK上去的(主要是net/ipv4/netfilter目录下的ip_fw_compat.c,ip_fw_compat_masq.c, ip_fw_compat_redir.c,ipchains_core.c,ipfwadm_core.c这几个文件)。 一个重要的变化是原有的INPUT,OUTPUT(原来是小写的input,ouput)链(现在应称为表?)的位置变了,原来的input,output的位置相当于现在的NF_IP_PRE_ROUTING,NF_IP_POST_ROUTING 。原有的结构确实不太合理,转发的包要经过三条链,现在INPUT专指去往本机的,OUPUT专指从本机发出的,而FOWARD仍旧是转发的包。 举两个简单的例子: 1.作地址伪装(场景:对外通过拨号连接internet)注意原来的MASQ变成好长的MASQUERATE,而伪装相当于SNAT,因此位置是在POSTROUTING: iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERATE 2.还有一个限制包速率的功能比较好玩,例如如下规则: iptables -A FORWARD -p tcp --syn -m limit --limit 1/s -j ACCEPT 简单的说就是在转发时(-A FORWARD:因为是防火墙嘛),如果是tcp协议,且有syn标记(-p tcp --syn),可以限制为每秒一个(-m limit --limit 1/s ),行动项是ACCEPT。最后连起来意义就是每秒只允许转发一个tcp连接请求。 五.后记 netfilter还提供了许多新功能,如可以将包转发到应用层,由应用程序进行处理等,可目前我还没有分析多少,慢慢抽出点时间看吧。唉,尽管以前看过ipchains的代码,但netfilter实在变动太大了,一切都要从头看起 最后,当然要感谢Rusty Russell,netfilter项目的负责人,不仅为我们提供了这个强大好用的工具,还写了大量非常优秀的文档。 参考文献: [1.] Linux 2.4 Packet Filtering HOWTO Rusty Russell, mailing list netfilter@lists.samba.org v1.0.1 Mon May 1 18:09:31 CST 2000 [2.] Linux IPCHAINS-HOWTO Paul Russell, ipchains@rustcorp.com v1.0.7, Fri Mar 12 13:46:20 CST 1999 [3.] Linux 2.4 NAT HOWTO Rusty Russell, mailing list netfilter@lists.samba.org v1.0.1 Mon May 1 18:38:22 CST 2000 [4.] Linux netfilter Hacking HOWTO Rusty Russell, mailing list netfilter@lists.samba.org v1.0.1 Sat Jul 1 18:24:41 EST 2000 [5.] Writing a Module for netfilter by Paul "Rusty" Russell Linux Magazine June 2000 http://www.linux-mag.com/2000-06/gear_01.html [6.] Salvatore Sanfilippo<antirez@invece.org>;写的一份netfilter sample,但可惜我找不到出处了,只剩下手头一份打印稿,But anyway,thanks to Salvatore. [目录] -------------------------------------------------------------------------------- ip/tables 1 /* 2 * 25-Jul-1998 Major changes to allow for ip chain table 3 * 4 * 3-Jan-2000 Named tables to allow packet selection for different uses. 5 */ 6 7 /* 8 * Format of an IP firewall descriptor 9 * 注意这里的说明:IP地址和掩码是按照网络字节存储(大端存储)标志字节和端口号是按照主机字节序存储(依主机硬件结构而定) 10 * src, dst, src_mask, dst_mask are always stored in network byte order. 11 * flags are stored in host byte order (of course). 12 * Port numbers are stored in HOST byte order. 13 */ 14 15 #ifndef _IPTABLES_H 16 #define _IPTABLES_H 17 18 #ifdef __KERNEL__ 19 #include linux/if.h 20 #include linux/types.h 21 #include linux/in.h 22 #include linux/ip.h 23 #include linux/skbuff.h 24 #endif 25 #include linux/netfilter_ipv4.h 26 27 #define IPT_FUNCTION_MAXNAMELEN 30 28 #define IPT_TABLE_MAXNAMELEN 32 29 这个结构存储与IP头部有关的防火墙规则信息。这里的注释说“这个结构无须填充零字节”,就是说这个结构的大小正好是4的倍数。这里由于IFNAMSIZ等于16,所以整个结构大小确实是4的倍数。 30 /* Yes, Virginia, you have to zero the padding. */ 31 struct ipt_ip { 32 /* Source and destination IP addr */ 33 struct in_addr src, dst; 34 /* Mask for src and dest IP addr */ 35 struct in_addr smsk, dmsk; 36 char iniface[IFNAMSIZ], outiface[IFNAMSIZ]; 37 unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ]; 38 39 /* Protocol, 0 = ANY */ 40 u_int16_t proto; 41 42 /* Flags word */ 43 u_int8_t flags; 44 /* Inverse flags */ 45 u_int8_t invflags; 46 }; 47这个结构存储match的信息,这里的匹配主要是指与IP无关的防火墙规则信息。由系统缺省设置的匹配主要有三个“tcp”、“udp”,“icmp”,我在分析ip_tables.c时将详细描述。 48 struct ipt_entry_match 49 { 50 union { 51 struct { 52 u_int16_t match_size; 53 54 /* Used by userspace */ 55 char name[IPT_FUNCTION_MAXNAMELEN]; 56 } user; 57 struct { 58 u_int16_t match_size; 59 60 /* Used inside the kernel */ 61 struct ipt_match *match; 62 } kernel; 63 64 /* Total length */ 65 u_int16_t match_size; 66 } u; 67 68 unsigned char data[0]; 69 }; 70 target结构信息,是决定一个分组命运的信息。也可以理解为action信息,其意义是指当一个分组与rule和match信息匹配后,如何处置该分组。处置方法一般有三种:一,命令常数,比如DROP ACCEPT等等;二 系统预定义的模块处理函数,比如”SNAT DNAT"等等;第三种是用户自己写模块函数。 71 struct ipt_entry_target 72 { 73 union { 74 struct { 75 u_int16_t target_size; 76 77 /* Used by userspace */ 78 char name[IPT_FUNCTION_MAXNAMELEN]; 79 } user; 80 struct { 81 u_int16_t target_size; 82 83 /* Used inside the kernel */ 84 struct ipt_target *target; 85 } kernel; 86 87 /* Total length */ 88 u_int16_t target_size; 89 } u; 90 91 unsigned char data[0]; 92 }; 93这个结构已经很明显给出了target的形式:命令常数、或者模块函数。 94 struct ipt_standard_target 95 { 96 struct ipt_entry_target target; 97 int verdict; 98 }; 99 计数器结构,每一个rule都有一个计数器结构用来统计匹配该条规则的分组数目和字节数目。为基于统计的安全工具提供分析基础。 100 struct ipt_counters 101 { 102 u_int64_t pcnt, bcnt; /* Packet and byte counters */ 103 }; 104 标志字段,各个常数后面的注释已经给出了明确的解释,这里不再赘述。 105 /* Values for "flag" field in struct ipt_ip (general ip structure). */ 106 #define IPT_F_FRAG 0x01 /* Set if rule is a fragment rule */ 107 #define IPT_F_MASK 0x01 /* All possible flag bits mask. */ 108 109 /* Values for "inv" field in struct ipt_ip. */ 110 #define IPT_INV_VIA_IN 0x01 /* Invert the sense of IN IFACE. */ 111 #define IPT_INV_VIA_OUT 0x02 /* Invert the sense of OUT IFACE */ 112 #define IPT_INV_TOS 0x04 /* Invert the sense of TOS. */ 113 #define IPT_INV_SRCIP 0x08 /* Invert the sense of SRC IP. */ 114 #define IPT_INV_DSTIP 0x10 /* Invert the sense of DST OP. */ 115 #define IPT_INV_FRAG 0x20 /* Invert the sense of FRAG. */ 116 #define IPT_INV_PROTO 0x40 /* Invert the sense of PROTO. */ 掩码标志。用法是当出现超出掩码范围的标志时,确认是非法标志。 117 #define IPT_INV_MASK 0x7F /* All possible flag bits mask. */ 118 其实这个结构的构成这里的注释已经说的很清楚,但是从论坛上有人问"关于netfilter的问题“时,可以看出很多人还是不理解。与前面ipchains版本防火墙不同的是iptables的防火墙规则构成发生了变化。ipchains的构成是rule+target,而iptables的构成是ip匹配信息+match+target。同时iptables构成的每一个部分都是可变大小的,由于经常出现”char XXX[0]“就可以看出。但是我个人认为规则的组织有点不好理解,它经常是先分配一段空间,然后将规则一条一条放入。如同文件系统存放变长记录的文件时,总要在记录中放入记录长度,以便以后取出记录,这里iptables正是使用这种方法,在每个规则中都放入长度字段,这样方便提取各个组成部分和计算下一条规则的位置。 119 /* This structure defines each of the firewall rules. Consists of 3 120 parts which are 1) general IP header stuff 2) match specific 121 stuff 3) the target to perform if the rule matches */ 122 struct ipt_entry 123 { 124 struct ipt_ip ip; 125 126 /* Mark with fields that we care about. */ 127 unsigned int nfcache; 128下面两个字段用来计算target的位置和下一条规则的位置。 129 /* Size of ipt_entry + matches */ 130 u_int16_t target_offset; 131 /* Size of ipt_entry + matches + target */ 132 u_int16_t next_offset; 133 这个字段的存在,为发现规则中存在”环路“提供手段。 134 /* Back pointer */ 135 unsigned int comefrom; 136 137 /* Packet and byte counters. */ 138 struct ipt_counters counters; 139 140 /* The matches (if any), then the target. */ 141 unsigned char elems[0]; 142 }; 143 144 /* 145 * New IP firewall options for [gs]etsockopt at the RAW IP level. 146 * Unlike BSD Linux inherits IP options so you don't have to use a raw 147 * socket for this. Instead we check rights in the calls. */ 定义提供给set/getsockopt系统调用的命令常数的基常数。 148 #define IPT_BASE_CTL 64 /* base for firewall socket options */ 149 150 #define IPT_SO_SET_REPLACE (IPT_BASE_CTL) 151 #define IPT_SO_SET_ADD_COUNTERS (IPT_BASE_CTL + 1) 152 #define IPT_SO_SET_MAX IPT_SO_SET_ADD_COUNTERS 153 154 #define IPT_SO_GET_INFO (IPT_BASE_CTL) 155 #define IPT_SO_GET_ENTRIES (IPT_BASE_CTL + 1) 156 #define IPT_SO_GET_MAX IPT_SO_GET_ENTRIES 157 158 /* CONTINUE verdict for targets */ 159 #define IPT_CONTINUE 0xFFFFFFFF 160 161 /* For standard target */ 162 #define IPT_RETURN (-NF_MAX_VERDICT - 1) 163 Tcp匹配规则信息。 164 /* TCP matching stuff */ 165 struct ipt_tcp 166 { 167 u_int16_t spts[2]; /* Source port range. */ 168 u_int16_t dpts[2]; /* Destination port range. */ 169 u_int8_t option; /* TCP Option iff non-zero*/ 170 u_int8_t flg_mask; /* TCP flags mask byte */ 171 u_int8_t flg_cmp; /* TCP flags compare byte */ 172 u_int8_t invflags; /* Inverse flags */ 173 }; 174 tcp的取反标志值。 175 /* Values for "inv" field in struct ipt_tcp. */ 176 #define IPT_TCP_INV_SRCPT 0x01 /* Invert the sense of source ports. */ 177 #define IPT_TCP_INV_DSTPT 0x02 /* Invert the sense of dest ports. */ 178 #define IPT_TCP_INV_FLAGS 0x04 /* Invert the sense of TCP flags. */ 179 #define IPT_TCP_INV_OPTION 0x08 /* Invert the sense of option test. */ 180 #define IPT_TCP_INV_MASK 0x0F /* All possible flags. */ 181 udp匹配规则信息 182 /* UDP matching stuff */ 183 struct ipt_udp 184 { 185 u_int16_t spts[2]; /* Source port range. */ 186 u_int16_t dpts[2]; /* Destination port range. */ 187 u_int8_t invflags; /* Inverse flags */ 188 }; 189 190 /* Values for "invflags" field in struct ipt_udp. */ 191 #define IPT_UDP_INV_SRCPT 0x01 /* Invert the sense of source ports. */ 192 #define IPT_UDP_INV_DSTPT 0x02 /* Invert the sense of dest ports. */ 193 #define IPT_UDP_INV_MASK 0x03 /* All possible flags. */ 194 195 /* ICMP matching stuff */ ICMP匹配规则信息 196 struct ipt_icmp 197 { 198 u_int8_t type; /* type to match */ 199 u_int8_t code[2]; /* range of code */ 200 u_int8_t invflags; /* Inverse flags */ 201 }; 202 203 /* Values for "inv" field for struct ipt_icmp. */ 204 #define IPT_ICMP_INV 0x01 /* Invert the sense of type/code test */ 205 这个结构实质上用户通过getsockopt系统调用获取table信息时所传递参数的类型。 206 /* The argument to IPT_SO_GET_INFO */ 207 struct ipt_getinfo 208 { 209 /* Which table: caller fills this in. */ 210 char name[IPT_TABLE_MAXNAMELEN]; 211 212 /* Kernel fills these in. */ 213 /* Which hook entry points are valid: bitmask */ 214 unsigned int valid_hooks; 215 216 /* Hook entry points: one per netfilter hook. */ 217 unsigned int hook_entry[NF_IP_NUMHOOKS]; 218 219 /* Underflow points. */ 220 unsigned int underflow[NF_IP_NUMHOOKS]; 221 222 /* Number of entries */ 223 unsigned int num_entries; 224 225 /* Size of entries. */ 226 unsigned int size; 227 }; 228 这个结构是用户通过系统调用更换table是所传递的参数类型。 229 /* The argument to IPT_SO_SET_REPLACE. */ 230 struct ipt_replace 231 { 232 /* Which table. */ 233 char name[IPT_TABLE_MAXNAMELEN]; 234 235 /* Which hook entry points are valid: bitmask. You can't 236 change this. */ 237 unsigned int valid_hooks; 238 239 /* Number of entries */ 240 unsigned int num_entries; 241 242 /* Total size of new entries */ 243 unsigned int size; 244 245 /* Hook entry points. */ 246 unsigned int hook_entry[NF_IP_NUMHOOKS]; 247 248 /* Underflow points. */ 249 unsigned int underflow[NF_IP_NUMHOOKS]; 250 251 /* Information about old entries: */ 252 /* Number of counters (must be equal to current number of entries). */ 253 unsigned int num_counters; 254 /* The old entries' counters. */ 255 struct ipt_counters *counters; 256 257 /* The entries (hang off end: not really an array). */ 258 struct ipt_entry entries[0]; 259 }; 260 这个更改计数器时传递的参数类型。 261 /* The argument to IPT_SO_ADD_COUNTERS. */ 262 struct ipt_counters_info 263 { 264 /* Which table. */ 265 char name[IPT_TABLE_MAXNAMELEN]; 266 267 unsigned int num_counters; 268 269 /* The counters (actually `number' of these). */ 270 struct ipt_counters counters[0]; 271 }; 272 这个是想获取防火墙规则时,传递给系统调用的参数类型。 273 /* The argument to IPT_SO_GET_ENTRIES. */ 274 struct ipt_get_entries 275 { 276 /* Which table: user fills this in. */ 277 char name[IPT_TABLE_MAXNAMELEN]; 278 279 /* User fills this in: total entry size. */ 280 unsigned int size; 281 282 /* The entries. */ 283 struct ipt_entry entrytable[0]; 284 }; 285 286 /* Standard return verdict, or do jump. */ 287 #define IPT_STANDARD_TARGET "" 288 /* Error verdict. */ 289 #define IPT_ERROR_TARGET "ERROR" 290 现面定义了一些使用例程 291 /* Helper functions */ 获取一条防火墙规则的target位置 292 extern __inline__ struct ipt_entry_target * 293 ipt_get_target(struct ipt_entry *e) 294 { 295 return (void *)e + e->;target_offset; 296 } 297 下面的宏遍历处理一条防火墙规则的所有匹配。我已经说过每一条防火墙规则在iptables中分为三部分,而且每一部分的大小都是可变的。比如match部分,它本身可以有多个match项。 298 /* fn returns 0 to continue iteration */ 299 #define IPT_MATCH_ITERATE(e, fn, args...) \ 300 ({ \ 301 unsigned int __i; \ 302 int __ret = 0; \ 303 struct ipt_entry_match *__m; \ 这个for语句我来解释一下:首先__i取值为ipt_entry结构的大小,实质上就是match匹配的开始处的偏移地址,将其与e相加就得到了match匹配的地址,然后调用fn处理这个匹配。如果函数返回值为零,当前匹配的偏移地址加上当前匹配的大小,如果不超过target的偏移地址,则继续处理下一条匹配。 304 \ 305 for (__i = sizeof(struct ipt_entry); \ 306 __i target_offset; \ 307 __i += __m->;u.match_size) { \ 308 __m = (void *)(e) + __i; \ 309 \ 310 __ret = fn(__m , ## args); \ 311 if (__ret != 0) \ 312 break; \ 313 } \ 314 __ret; \ 315 }) 316 这个宏处理一个table中的所有防火墙规则。对比对上一个宏的理解,这里我就不解释了。 317 /* fn returns 0 to continue iteration */ 318 #define IPT_ENTRY_ITERATE(entries, size, fn, args...) \ 319 ({ \ 320 unsigned int __i; \ 321 int __ret = 0; \ 322 struct ipt_entry *__e; \ 323 \ 324 for (__i = 0; __i next_offset) { \ 325 __e = (void *)(entries) + __i; \ 326 \ 327 __ret = fn(__e , ## args); \ 328 if (__ret != 0) \ 329 break; \ 330 } \ 331 __ret; \ 332 }) 333 334 /* 335 * Main firewall chains definitions and global var's definitions. 336 */ 337 #ifdef __KERNEL__ 338 339 #include · 340 extern void ipt_init(void) __init; 341 所有的匹配处理都注册到一个match处理链表中,链表结点的类型就是这里的结构类型。当处理匹配时都是调用这里注册的处理函数。每个结点实质上由三个函数构成,一个匹配处理函数,一个合法性检查函数,一个析构函数。最后一个是反身指针,指针的作用如注释所示。 342 struct ipt_match 343 { 344 struct list_head list; 345 346 const char name[IPT_FUNCTION_MAXNAMELEN]; 347 348 /* Return true or false: return FALSE and set *hotdrop = 1 to 349 force immediate packet drop. */ 350 int (*match)(const struct sk_buff *skb, 351 const struct net_device *in, 352 const struct net_device *out, 353 const void *matchinfo, 354 int offset, 355 const void *hdr, 356 u_int16_t datalen, 357 int *hotdrop); 358 359 /* Called when user tries to insert an entry of this type. */ 360 /* Should return true or false. */ 361 int (*checkentry)(const char *tablename, 362 const struct ipt_ip *ip, 363 void *matchinfo, 364 unsigned int matchinfosize, 365 unsigned int hook_mask); 366 367 /* Called when entry of this type deleted. */ 368 void (*destroy)(void *matchinfo, unsigned int matchinfosize); 369 370 /* Set this to THIS_MODULE if you are a module, otherwise NULL */ 371 struct module *me; 372 }; 373 和match一样,所有的target都注册到这个结构类型的全局链表中,每个target的处理函数都是这里注册的函数。和上面的解释一样,这里也主要包含三个函数指针。 374 /* Registration hooks for targets. */ 375 struct ipt_target 376 { 377 struct list_head list; 378 379 const char name[IPT_FUNCTION_MAXNAMELEN]; 380 381 /* Returns verdict. */ 382 unsigned int (*target)(struct sk_buff **pskb, 383 unsigned int hooknum, 384 const struct net_device *in, 385 const struct net_device *out, 386 const void *targinfo, 387 void *userdata); 388 389 /* Called when user tries to insert an entry of this type: 390 hook_mask is a bitmask of hooks from which it can be 391 called. */ 392 /* Should return true or false. */ 393 int (*checkentry)(const char *tablename, 394 const struct ipt_entry *e, 395 void *targinfo, 396 unsigned int targinfosize, 397 unsigned int hook_mask); 398 399 /* Called when entry of this type deleted. */ 400 void (*destroy)(void *targinfo, unsigned int targinfosize); 401 402 /* Set this to THIS_MODULE if you are a module, otherwise NULL */ 403 struct module *me; 404 }; 405 注册函数 406 extern int ipt_register_target(struct ipt_target *target); 407 extern void ipt_unregister_target(struct ipt_target *target); 408 409 extern int ipt_register_match(struct ipt_match *match); 410 extern void ipt_unregister_match(struct ipt_match *match); 411 table结构 412 /* Furniture shopping... */ 413 struct ipt_table 414 { 415 struct list_head list; 416 417 /* A unique name... */ 418 char name[IPT_TABLE_MAXNAMELEN]; 419 420 /* Seed table: copied in register_table */ 421 struct ipt_replace *table; 422 423 /* What hooks you will enter on */ 424 unsigned int valid_hooks; 425 426 /* Lock for the curtain */ 427 rwlock_t lock; 428 429 /* Man behind the curtain... */ 430 struct ipt_table_info *private; 431 }; 432 小结iptables的防火墙组织结构: 现在我们可以给出iptables防火墙规则的组织结构了。第一级是table级,每一个防火墙可以有多个table;第二级是hook级,每一个table都有一个hook集合,每个hook都有一个防火墙规则链;第三级基本规则级,基本规则级的规则包括三部分,IP规则信息、匹配规则信息和target。而这三个组成部分的每一个都可以包括同类型的多个部分规则。 433 extern int ipt_register_table(struct ipt_table *table); 434 extern void ipt_unregister_table(struct ipt_table *table); 435 extern unsigned int ipt_do_table(struct sk_buff **pskb, 436 unsigned int hook, 437 const struct net_device *in, 438 const struct net_device *out, 439 struct ipt_table *table, 440 void *userdata); 441 有关table结构对齐的宏。 442 #define IPT_ALIGN(s) (((s) + (__alignof__(struct ipt_entry)-1)) ~(__alignof__(struct ipt_entry)-1)) 443 #endif /*__KERNEL__*/ 444 #endif /* _IPTABLES_H */ 445 西安交通大学 王灏 [目录] -------------------------------------------------------------------------------- 防火墙技术分析讲义 防火墙技术分析讲义 yawl@docshow.net 一 基本概念 1.1 防火墙分类: 包过滤 代理(应用层网关) 1.2 代理: 两个连接(browser与proxy之间,proxy与web server之间)。 工作在应用层。 直接发往服务器的包: GET / HTTP/1.1 Accept: */* Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Host: www.lisoleg.net Connection: Keep-Alive 往代理发出的包: GET http://www.lisoleg.net/ HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/msword, */* Accept-Language: zh-cn Accept-Encoding: gzip, deflate If-Modified-Since: Thu, 14 Dec 2000 07:24:52 GMT If-None-Match: "8026-185-3a3875c4" User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Host: www.lisoleg.net Proxy-Connection: Keep-Alive 增强: cache 1.3 包过滤 单IP包检测 缺陷:无状态 1.4 增强1-状态检测(Stateful Inspection),又称动态包过滤(dynamic packet filtering) 1.4.1 规则表和动态状态表 1.4.2 ftp的例子: A 4847->;B 21 PORT 192,168,7,60,18,241 B 21->;A 4847 PORT command successful. B 20->;A 4849 syn >; A classic example is transferring files using FTP. The firewall remembers the details of the >; incoming request to get a file from an FTP server. The firewall then tracks the back-channel >; request (the FTP Port command) by the server for transferring information back to the client. >; As long as the information agrees (same IP addresses, no changes in port numbers, and no >; non-FTP requests), the firewall allows the traffic. After the transfer is complete, the >; firewall closes the ports involved. 1.4.3 两种实现方法: 1.4.3.1 checkpoint FW1,netfilter 1.4.3.2 动态添加规则(ipchains patch) >; I believe it does exactly what I want: Installing a temporary >; "backward"-rule to let packets in as a response to an >; outgoing request. 1.5 增强2-地址转换: 1.5.1 静态NAT 1.5.2 动态NAT 1.5.3 地址伪装 1.6 增强3-VPN: 位置的优越性 二 Linux下防火墙的实现之一(2.2内核): 2.1 截获位置: 网络层 ---------------------------------------------------------------- | ACCEPT/ lo interface | v REDIRECT _______ | -->; C -->; S -->; ______ -->; D -->; ~~~~~~~~ -->;|forward|---->; _______ -->; h a |input | e {Routing } |Chain | |output |ACCEPT e n |Chain | m {Decision} |_______| --->;|Chain | c i |______| a ~~~~~~~~ | | ->;|_______| k t | s | | | | | s y | q | v | | | u | v e v DENY/ | | v m | DENY/ r Local Process REJECT | | DENY/ | v REJECT a | | | REJECT | DENY d --------------------- | v e ----------------------------- DENY 2.2 提炼出的代码: 输入检测: /* * Main IP Receive routine. */ int ip_rcv(struct sk_buff *skb, struct device *dev, struct packet_type *pt) { #ifdef CONFIG_FIREWALL int fwres; u16 rport; #endif /* CONFIG_FIREWALL */ ...... #ifdef CONFIG_FIREWALL /* * See if the firewall wants to dispose of the packet. * * We can't do ICMP reply or local delivery before routing, * so we delay those decisions until after route. --RR */ fwres = call_in_firewall(PF_INET, dev, iph, &rport, &skb); if (fwres < FW_ACCEPT && fwres != FW_REJECT) goto drop; iph = skb->;nh.iph; #endif /* CONFIG_FIREWALL */ ...... #ifdef CONFIG_FIREWALL if (fwres == FW_REJECT) { icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); goto drop; } #endif /* CONFIG_FIREWALL */ return skb->;dst->;input(skb); //根据路由查找的结果决定是转发(ip_forward)还是发往上层(ip_local_deliver) drop: kfree_skb(skb); //如果规则匹配的结果是FW_REJECT,FW_BLOCK,丢弃此包 return(0); } 转发检测: int ip_forward(struct sk_buff *skb) { ... #ifdef CONFIG_FIREWALL fw_res=call_fw_firewall(PF_INET, dev2, iph, NULL, &skb); switch (fw_res) { case FW_ACCEPT: case FW_MASQUERADE: break; case FW_REJECT: icmp_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0); /* fall thru */ default: kfree_skb(skb); return -1; } #endif ... } 输出检测:(不同的上层协议走不同的流程,因此检测点较多) UDP/RAW/ICMP报文:ip_build_xmit TCP报文:ip_queue_xmit 转发的包:ip_forward 其他:ip_build_and_send_pkt 实际的匹配: /* * Returns one of the generic firewall policies, like FW_ACCEPT. * * The testing is either false for normal firewall mode or true for * user checking mode (counters are not updated, TOS & mark not done). */ static int ip_fw_check(struct iphdr *ip, //IP头位置 const char *rif, //出口网卡的名字 __u16 *redirport, //端口转发时用到 struct ip_chain *chain, //规则链的名字 struct sk_buff *skb, //要检测的数据包 unsigned int slot, int testing) //见函数本身的注释 调用举例: call_in_firewall实际调用ipfw_input_check,而ipfw_input_check中有: int ipfw_input_check(struct firewall_ops *this, int pf, struct device *dev, void *phdr, void *arg, struct sk_buff **pskb) { return ip_fw_check(phdr, dev->;name, arg, IP_FW_INPUT_CHAIN, *pskb, SLOT_NUMBER(), 0); } 实际流程: |
系统调用 在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。进程使用寄存器中适当的值跳转到内核中事先定义好的代码中执行,(当然,这些代码是只读的)。在Intel结构的计算机中,这是由中断0x80实现的。 进程可以跳转到的内核中的位置叫做system_call。在此位置的过程检查系统调用号,它将告诉内核进程请求的服务是什么。然后,它再查找系统调用表sys_call_table,找到希望调用的内核函数的地址,并调用此函数,最后返回。 所以,如果希望改变一个系统调用的函数,需要做的是编写一个自己的函数,然后改变sys_call_table中的指针指向该函数,最后再使用cleanup_module将系统调用表恢复到原来的状态 [目录] -------------------------------------------------------------------------------- 系统调用简述 linux里面的每个系统调用是靠一些宏,,一张系统调用表,一个系统调用入口来完成的。 [目录] -------------------------------------------------------------------------------- 宏 宏就是_syscallN(type,name,x...),N是系统调用所需的参数数目,type是返回类型,name即面向用户的系统调用函数名,x...是调用参数,个数即为N。 例如: #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1,type2 arg2,type3 arg3) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3))); \ if (__res>;=0) \ return (type) __res; \ errno=-__res; \ return -1; \ } (这是2.0.33版本) 这些宏定义于include\asm\Unistd.h,这就是为什么你在程序中要包含这个头文件的原因。该文件中还以__NR_name的形式定义了164个常数,这些常数就是系统调用函数name的函数指针在系统调用表中的偏移量。 [目录] -------------------------------------------------------------------------------- 系统调用表 系统调用表定义于entry.s的最后。 这个表按系统调用号(即前面提到的__NR_name)排列了所有系统调用函数的指针,以供系统调用入口函数查找。从这张表看得出,linux给它所支持的系统调用函数取名叫sys_name。 [目录] -------------------------------------------------------------------------------- 系统调用入口函数 系统调用入口函数定义于entry.s: ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL #ifdef __SMP__ ENTER_KERNEL #endif movl $-ENOSYS,EAX(%esp) cmpl $(NR_syscalls),%eax jae ret_from_sys_call movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax testl %eax,%eax je ret_from_sys_call #ifdef __SMP__ GET_PROCESSOR_OFFSET(%edx) movl SYMBOL_NAME(current_set)(,%edx),%ebx #else movl SYMBOL_NAME(current_set),%ebx #endif andl $~CF_MASK,EFLAGS(%esp) movl %db6,%edx movl %edx,dbgreg6(%ebx) testb $0x20,flags(%ebx) jne 1f call *%eax movl %eax,EAX(%esp) jmp ret_from_sys_call 这段代码现保存所有的寄存器值,然后检查调用号(__NR_name)是否合法(在系统调用表中查找),找到正确的函数指针后,就调用该函数(即你真正希望内核帮你运行的函数)。运行返回后,将调用ret_from_sys_call,这里就是著名的进程调度时机之一。 当在程序代码中用到系统调用时,编译器会将上面提到的宏展开,展开后的代码实际上是将系统调用号放入ax后移用int 0x80使处理器转向系统调用入口,然后查找系统调用表,进而由内核调用真正的功能函数。 自己添加过系统调用的人可能知道,要在程序中使用自己的系统调用,必须显示地应用宏_syscallN。 而对于linux预定义的系统调用,编译器在预处理时自动加入宏_syscall3(int,ioctl,arg1,arg2,arg3)并将其展开。所以,并不是ioctl本身是宏替换符,而是编译器自动用宏声明了ioctl这个函数。 [目录] -------------------------------------------------------------------------------- 系统调用实现过程 [目录] -------------------------------------------------------------------------------- 函数名约定 系统调用响应函数的函数名约定 函数名以“sys_”开头,后跟该系统调用的名字,由此构成164个形似sys_name()的函数名。因此,系统调用ptrace()的响应函数是sys_ptrace() (kernel/ptrace.c)。 [目录] -------------------------------------------------------------------------------- 系统调用号 系统调用号 文件include/asm/unistd.h为每个系统调用规定了唯一的编号: #define __NR_setup 0 #define __NR_exit 1 #define __NR_fork 2 … … #define __NR_ptrace 26 以系统调用号__NR_name作为下标,找出系统调用表sys_call_table (arch/i386/kernel/entry.S)中对应表项的内容,正好就是该系统调用的响应函数sys_name的入口地址。 [目录] -------------------------------------------------------------------------------- 系统调用表 系统调用表 系统调用表sys_call_table (arch/i386/kernel/entry.S)形如: ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) /* 0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) … … .long SYMBOL_NAME(sys_stime) /* 25 */ .long SYMBOL_NAME(sys_ptrace) … … sys_call_table记录了各sys_name函数(共166项,其中2项无效)在表中的位子。有了这张表,很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。NR_syscalls(即256)表示最多可容纳的系统调用个数。这样,余下的90项就是可供用户自己添加的系统调用空间。 [目录] -------------------------------------------------------------------------------- 从ptrace系统调用命令到INT 0X80中断请求的转换 从ptrace系统调用命令到INT 0X80中断请求的转换 宏定义syscallN()(include/asm/unistd.h)用于系统调用的格式转换和参数的传递。 #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \ type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3)),"S" ((long)(arg4))); \ __syscall_return(type,__res); \ } N取0与5之间任意整数。参数个数为N的系统调用由syscallN负责格式转换和参数传递。例如,ptrace()有四个参数,它对应的格式转换宏就是syscall4()。 syscallN()第一个参数说明响应函数返回值的类型,第二个参数为系统调用的名称(即name),其余的参数依次为系统调用参数的类型和名称。例如, _syscall4(int, ptrace, long request, long pid, long addr, long data) 说明了系统调用命令 int sys_ptrace(long request, long pid, long addr, long data) 宏定义的余下部分描述了启动INT 0X80和接收、判断返回值的过程。也就是说,以系统调用号对EAX寄存器赋值,启动INT 0X80。规定返回值送EAX寄存器。函数的参数压栈,压栈顺序见下表: 参数 参数在堆栈的位置 传递参数的寄存器 arg1 00(%esp) ebx arg2 04(%esp) ecx arg3 08(%esp) edx arg4 0c(%esp) esi arg5 10(%esp) edi 若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。 [目录] -------------------------------------------------------------------------------- 系统调用功能模块的初始化 系统调用功能模块的初始化 对系统调用的初始化也即对INT 0X80的初始化。系统启动时,汇编子程序setup_idt(arch/i386/kernel/head.S)准备了张256项的idt 表,由start_kernel()(init/main.c)、trap_init()(arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80, &system_call)(include/asm/system.h)设置0X80号软中断的服务程序为system_call。system_call(arch/i386/kernel/entry.S)就是所有系统调用的总入口。 [目录] -------------------------------------------------------------------------------- 内核服务 LINUX内部是如何分别为各种系统调用服务的 当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数由各通用寄存器传递。然后执行INT 0X80,以核心态进入入口地址system_call。 ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL #ifdef __SMP__ ENTER_KERNEL #endif movl $-ENOSYS,EAX(%esp) cmpl $(NR_syscalls),%eax jae ret_from_sys_call movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax testl %eax,%eax je ret_from_sys_call #ifdef __SMP__ GET_PROCESSOR_OFFSET(%edx) movl SYMBOL_NAME(current_set)(,%edx),%ebx #else movl SYMBOL_NAME(current_set),%ebx #endif andl $~CF_MASK,EFLAGS(%esp) # clear carry - assume no errors movl %db6,%edx movl %edx,dbgreg6(%ebx) # save current hardware debugging status testb $0x20,flags(%ebx) # PF_TRACESYS jne 1f call *%eax movl %eax,EAX(%esp) # save the return value jmp ret_from_sys_call 从system_call入口的汇编程序的主要功能是: ·保存寄存器当前值(SAVE_ALL); ·检验是否为合法的系统调用; ·根据系统调用表_sys_call_table和EAX持有的系统调用号找出并转入系统调用响应函数; ·从该响应函数返回后,让EAX寄存器保存函数返回值,跳转至ret_from_sys_call(arch/i386/kernel/entry.S)。 ·最后,在执行位于用户程序中系统调用命令后面余下的指令之前,若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。 [目录] -------------------------------------------------------------------------------- 代码分析:mlock() 系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。 mlock调用的语法为: int sys_mlock(unsigned long start, size_t len); 初始化为: len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK; start &=PAGE_MASK; 其中mlock又调用do_mlock(),语法为: int do_mlock(unsigned long start, size_t len,int on); 初始化为: len=(len+~PAGE_MASK)&AGE_MASK; 由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK)的内存区域的页进行加锁。 sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。 内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。 内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面 mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下: ·ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。 ·EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。 ·EINVAL:输入参数len不是个合法的正数。 [目录] -------------------------------------------------------------------------------- 主要数据结构 1.mm_struct struct mm_struct { int count; pgd_t * pgd; /* 进程页目录的起始地址*/ unsigned long context; unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack, start_mmap; unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags; struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */ struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */ struct semaphore mmap_sem; } ·start_code、end_code:进程代码段的起始地址和结束地址。 ·start_data、end_data:进程数据段的起始地址和结束地址。 ·arg_start、arg_end:调用参数区的起始地址和结束地址。 ·env_start、env_end:进程环境区的起始地址和结束地址。 ·rss:进程内容驻留在物理内存的页面总数。 2. 虚存段(vma)数据结构:vm_area_atruct 虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述: struct vm_area_struct { struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; pgprot_t vm_page_prot; unsigned short vm_flags; /* AVL tree of VM areas per task, sorted by address */ short vm_avl_height; struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right; /* linked list of VM areas per task, sorted by address */ struct vm_area_struct * vm_next; /* for areas with inode, the circular list inode->;i_mmap */ /* for shm areas, the circular list of attaches */ /* otherwise unused */ struct vm_area_struct * vm_next_share; struct vm_area_struct * vm_prev_share; /* more */ struct vm_operations_struct * vm_ops; unsigned long vm_offset; struct inode * vm_inode; unsigned long vm_pte; /* shared mem */ }; vm_start;//所对应内存区域的开始地址 vm_end; //所对应内存区域的结束地址 vm_flags; //进程对所对应内存区域的访问权限 vm_avl_height;//avl树的高度 vm_avl_left; //avl树的左儿子 vm_avl_right; //avl树的右儿子 vm_next;// 进程所使用的按地址排序的vm_area链表指针 vm_ops;//一组对内存的操作 这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。 当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。 属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。 为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。 对平衡树mmap_avl的任何操作必须满足平衡树的一些规则: Consistency and balancing rulesJ(一致性和平衡规则): tree->;vm_avl_height==1+max(heightof(tree->;vm_avl_left),heightof( tree->;vm_avl_right)) abs( heightof(tree->;vm_avl_left) - heightof(tree->;vm_avl_right) ) <= 1 foreach node in tree->;vm_avl_left: node->;vm_avl_key <= tree->;vm_avl_key, foreach node in tree->;vm_avl_right: node->;vm_avl_key >;= tree->;vm_avl_key. 注:其中node->;vm_avl_key= node->;vm_end 对vma可以进行加锁、加保护、共享和动态扩展等操作。 [目录] -------------------------------------------------------------------------------- 重要常量 mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下: PAGE_SHIFT 12 // PAGE_SHIFT determines the page size PAGE_SIZE 0x1000 //1UL<<AGE_SHIFT PAGE_MASK ~(PAGE_SIZE-1) //a very useful constant variable RLIMIT_MEMLOCK 8 //max locked-in-memory address space VM_LOCKED 0x2000 //8*1024=8192, vm_flags的标志之一。 PF_SUPERPRIV 0x00000100 //512 [目录] -------------------------------------------------------------------------------- 代码函数功能分析 mlock系统调用代码函数功能分析 下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程): suser():如果用户有效(即current->;euid == 0 ),则设置进程标志为root优先权(current->;flags |= PF_SUPERPRIV),并返回1;否则返回0。 find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->;mmap_avl->;vm_start<=addr< mm->;mmap_avl->;vm_end的vma,如果成功则返回此vma;否则返回空null。 mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。 merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。 mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。 mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->;start和new_vma->;end)大小序列把新生成的new->;vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。 注:vma->;vm_offset+= vma->;vm_start-new_vma->;vm_start; mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->;vma插入到当前进程mm的mmap链或mmap_avl树中。 注:new_vma->;vm_offset= vma->;vm_offset+(new_vma->;vm_start-vma->;vm_start); mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->;vma和right->;vma插入到当前进程mm的mmap链或mmap_avl树中。 注:vma->;vm_offset += vma->;vm_start-left_vma->;vm_start; right_vma->;vm_offset += right_vma->;vm_start-left_vma->;vm_start; kmalloc():常用的一个内核函数 insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->;inode的i_mmap环(循环共享链)中。 avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。 avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。 down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。 down定义在/include/linux/sched.h中: extern inline void down(struct semaphore * sem) { if (sem->;count <= 0) __down(sem); sem->;count--; } up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。 up定义在/include/linux/sched.h中: extern inline void up(struct semaphore * sem) { sem->;count++; wake_up(&sem->;wait); } kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。 avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。 avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。 remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->;immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->;immap环中删除的该结点。 [目录] -------------------------------------------------------------------------------- 添加新调用 [目录] -------------------------------------------------------------------------------- 例子一 深入LINUX内核:为你的LINUX增加一条系统调用 充分利用LINUX开放源码的特性,我们可以轻易地对它进行修改,使我们能够随心所欲驾驭LINUX,完成一个真正属于自己的操作系统,这种感觉使无与伦比的,下面通过为LINUX增加一个系统调用来展示LINUX作为一个开放源码操作系统的强大魅力。 首先,让我们简单地分析一下LINUX中与系统调用的相关的部分: LINUX的系统调用的总控程序是system_call,它是LINUX系统中所有系统调用的总入口,这个system_call是作为一个中断服务程序挂在中断0x80上,系统初始化时通过void init trap_init(void)调用一个宏set_system_ gate(SYSCALL_VERCTOR,&system_call)来对IDT表进行初始化,在0x80对应的中断描述符处填入system_call函数的地址,其中宏SYSCALL_VERCTOR就是0x80。 当发生一条系统调用时,由中断总控程序保存处理机状态,检查调用参数的合法性,然后根据系统调用向量在sys_call_table中找到相应的系统服务例程的地址,然后执行该服务例程,完成后恢复中断总控程序所保存的处理机状态,返回用户程序。 系统服务例程一般定义于kernel/sys.c中,系统调用向量定义在include/asm-386/unistd.h中,而sys_call _table表则定义在arch/i386/kernel/entry.S文件里。 现在我们知道增加一条系统调用我们首先要添加服务例程实现代码,然后在进行对应向量的申明,最后当然还要在sys_call_table表中增加一项以指明服务例程的入口地址。 OK,有了以上简单的分析,现在我们可以开始进行源码的修改,假设我们需要添加一条系统调用计算两个整数的平方和,系统调用名为add2,我们需要修改三个文件:kernel/sys.c , arch/i386/kernel/entry.S 和 include/asm-386/unistd.h。 1、修改kernel/sys.c ,增加服务例程代码: asmlinkage int sys_add2(int a , int b) { int c=0; c=a*a+b*b; return c; } 2、修改include/asm-386/unistd.h ,对我们刚才增加的系统调用申明向量,以使用户或系统进程能够找到这条系统调用,修改后文件如下所示: .... ..... #define _NR_sendfile 187 #define _NR_getpmsg 188 #define _NR_putmsg 189 #define _NR_vfork 190 #define _NR_add2 191 /* 这是我们添加的部分,191即向量 */ 3、修改include/asm-386/unistd.h , 将服务函数入口地址加入 sys_call_table,首先找到这么一段: .... ..... .long SYMBOL_NAME(sys_sendfile) .long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */ .long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */ .long SYMBOL_NAME(sys_vfork) /*190 */ .rept NR_syscalls-190 修改为如下: .... ..... .long SYMBOL_NAME(sys_sendfile) .long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */ .long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */ .long SYMBOL_NAME(sys_vfork) /*190 */ .long SYMBOL_NAME(sys_add2) <=我们的系统调用 .rept NR_syscalls-191 <=将190改为191 OK,大功告成,现在只需要重新编译你的LINUX内核,然后你的LINUX就有了一条新的系统调用int add2(int a, int b)。 [目录] -------------------------------------------------------------------------------- 例子二 如何在Linux中添加新的系统调用 系统调用是应用程序和操作系统内核之间的功能接口。其主要目的是使得用户可以使用操作系统提供的有关设备管理、输入/输入系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。 1 Linux系统调用机制 在Linux系统中,系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统切换为核心态,并安排异常处理程序的执行。 Linux用来实现系统调用异常的实际指令是: Int $0x80 这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。 为使系统调用的执行成为一项简单的任务,Linux提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。 这些宏指令具有类似下面的名称格式: _syscallN(parameters) 其中N是系统调用所需的参数数目,而parameters则用一组参数代替。这些参数使宏指令完成适合于特定的系统调用的扩展。例如,为了建立调用setuid()系统调用的函数,应该使用: _syscall1( int, setuid, uid_t, uid ) syscallN( )宏指令的第1个参数int说明产生的函数的返回值的类型是整型,第2个参数setuid说明产生的函数的名称。后面是系统调用所需要的每个参数。这一宏指令后面还有两个参数uid_t和uid分别用来指定参数的类型和名称。 另外,用作系统调用的参数的数据类型有一个限制,它们的容量不能超过四个字节。这是因为执行int $0x80指令进行系统调用时,所有的参数值都存在32位的CPU寄存器中。使用CPU寄存器传递参数带来的另一个限制是可以传送给系统调用的参数的数目。这个限制是最多可以传递5个参数。所以Linux一共定义了6个不同的_syscallN()宏指令,从_syscall0()、_syscall1()直到_syscall5()。 一旦_syscallN()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。 2 添加新的系统调用 如果用户在Linux中添加新的系统调用,应该遵循几个步骤才能添加成功,下面几个步骤详细说明了添加系统调用的相关内容。 (1) 添加源代码 第一个任务是编写加到内核中的源程序,即将要加到一个内核文件中去的一个函数,该函数的名称应该是新的系统调用名称前面加上sys_标志。假设新加的系统调用为mycall(int number),在/usr/src/linux/kernel/sys.c文件中添加源代码,如下所示: asmlinkage int sys_mycall(int number) { return number; } 作为一个最简单的例子,我们新加的系统调用仅仅返回一个整型值。 (2) 连接新的系统调用 添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。 在我们所用的Linux内核版本(RedHat 6.0,内核为2.2.5-15)中,第一个要修改的文件是: /usr/src/linux/include/asm-i386/unistd.h 该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。文件中每一行的格式如下: #define __NR_name NNN 其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码。应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下: #define __NR_mycall 191 系统调用号为191,之所以系统调用号是191,是因为Linux-2.2内核自身的系统调用号码已经用到190。 第二个要修改的文件是: /usr/src/linux/arch/i386/kernel/entry.S 该文件中有类似如下的清单: .long SYMBOL_NAME() 该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。我们在清单最后添加一行: .long SYMBOL_NAME(sys_mycall) (3) 重建新的Linux内核 为使新的系统调用生效,需要重建Linux的内核。这需要以超级用户身份登录。 #pwd /usr/src/linux # 超级用户在当前工作目录(/usr/src/linux)下,才可以重建内核。 #make config #make dep #make clearn #make bzImage 编译完毕后,系统生成一可用于安装的、压缩的内核映象文件: /usr/src/linux/arch/i386/boot/bzImage (4) 用新的内核启动系统 要使用新的系统调用,需要用重建的新内核重新引导系统。为此,需要修改/etc/lilo.conf文件,在我们的系统中,该文件内容如下: boot=/dev/hda map=/boot/map install=/boot/boot.b prompt timeout=50 image=/boot/vmlinuz-2.2.5-15 label=linux root=/dev/hdb1 read-only other=/dev/hda1 label=dos table=/dev/had 首先编辑该文件,添加新的引导内核: image=/boot/bzImage-new label=linux-new root=/dev/hdb1 read-only 添加完毕,该文件内容如下所示: boot=/dev/hda map=/boot/map install=/boot/boot.b prompt timeout=50 image=/boot/bzImage-new label=linux-new root=/dev/hdb1 read-only image=/boot/vmlinuz-2.2.5-15 label=linux root=/dev/hdb1 read-only other=/dev/hda1 label=dos table=/dev/hda 这样,新的内核映象bzImage-new成为缺省的引导内核。 为了使用新的lilo.conf配置文件,还应执行下面的命令: #cp /usr/src/linux/arch/i386/boot/zImage /boot/bzImage-new 其次配置lilo: # /sbin/lilo 现在,当重新引导系统时,在boot:提示符后面有三种选择:linux-new 、 linux、dos,新内核成为缺省的引导内核。 至此,新的Linux内核已经建立,新添加的系统调用已成为操作系统的一部分,重新启动Linux,用户就可以在应用程序中使用该系统调用了。 (5)使用新的系统调用 在应用程序中使用新添加的系统调用mycall。同样为实验目的,我们写了一个简单的例子xtdy.c。 /* xtdy.c */ #include <linux/unistd.h>; _syscall1(int,mycall,int,ret) main() { printf("%d \n",mycall(100)); } 编译该程序: # cc -o xtdy xtdy.c 执行: # xtdy 结果: # 100 注意,由于使用了系统调用,编译和执行程序时,用户都应该是超级用户身份。 (文/程仁田) |
更多推荐
已为社区贡献2条内容
所有评论(0)