linux0.11下的中断机制分析

引用的这篇文章很不错,作者对linux0.11的机制进行了系列化的详细解读. 可惜链接好像有问题,可以直接百度标题,进行查找.

http://orbt.blog.163.com/

异常就是控制流中的突变,用来响应处理器状态中的某些变化。当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序,这张表即中断描述符表IDT。本文将针对Linux0.11代码进行分析和调试,来了解中断机制,主要分析以下三个问题:

1.  中断描述符表的建立。

2.  一般中断的处理过程,以0x3号中断为例。

3.  系统调用的处理过程,以fork系统调用为例。

中断描述符表的建立

中断描述符表(IDT)的创建代码在boot/head.s中,与全局描述符表的创建类似,内核执行lidt idt_descr指令完成创建工作,全局变量idt_descr的定义如下:


idt_descr:

.word 256*8-1              # idt contains 256 entries

.long _idt

_idt: .fill 256,8,0            # idt is uninitialized

lidt指令为6字节操作数,它将_idt的地址加载进idtr寄存器,IDT被设置为包含256个8字节表项的描述符表。

中断描述符表的初始化工作主要通过宏_set_get来完成,它定义于include/asm/system.h中,如下:


#define _set_gate(gate_addr,type,dpl,addr) /

__asm__ ("movw %%dx,%%ax/n/t" /

"movw %0,%%dx/n/t" /

"movl %%eax,%1/n/t" /

"movl %%edx,%2" /

: /

: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), /

"o" (*((char *) (gate_addr))), /

"o" (*(4+(char *) (gate_addr))), /

"d" ((char *) (addr)),"a" (0x00080000))

/*设置中断门函数,特权级0,类型386中断门*/

#define set_intr_gate(n,addr) /

_set_gate(&idt[n],14,0,addr)

/*设置陷阱门函数,特权级0,类型386陷阱门*/

#define set_trap_gate(n,addr) /

_set_gate(&idt[n],15,0,addr)

/*设置系统调用函数,特权级3,类型386陷阱门*/

#define set_system_gate(n,addr) /

_set_gate(&idt[n],15,3,addr)

内核将用这些宏初始化IDT表,代码如下:


/*摘自kernel/traps.c,trap_init函数*/

set_trap_gate(0,&divide_error);

set_trap_gate(1,&debug);

set_trap_gate(2,&nmi);

set_system_gate(3,&int3);     /* int3-5 can be called from all */

set_system_gate(4,&overflow);

set_system_gate(5,&bounds);

set_trap_gate(6,&invalid_op);

set_trap_gate(7,&device_not_available);

set_trap_gate(8,&double_fault);

set_trap_gate(9,&coprocessor_segment_overrun);

set_trap_gate(10,&invalid_TSS);

set_trap_gate(11,&segment_not_present);

set_trap_gate(12,&stack_segment);

set_trap_gate(13,&general_protection);

set_trap_gate(14,&page_fault);

set_trap_gate(15,&reserved);

set_trap_gate(16,&coprocessor_error);

for (i=17;i<48;i++)

set_trap_gate(i,&reserved);

set_trap_gate(45,&irq13);

set_trap_gate(39,&parallel_interrupt);


/*摘自kernel/chr_drv/serial.c,rs_init函数*/

set_intr_gate(0x24,rs1_interrupt);

set_intr_gate(0x23,rs2_interrupt);


/*摘自kernel/chr_drv/console.c,con_init函数*/

set_trap_gate(0x21,&keyboard_interrupt);


/*摘自kernel/sched.c,sched_init函数*/

set_intr_gate(0x20,&timer_interrupt);

set_system_gate(0x80,&system_call);


/*摘自kernel/blk_drv/hd.c,hd_init函数*/

set_intr_gate(0x2E,&hd_interrupt);


/*摘自kernel/blk_drv/floppy.c,floppy_init函数*/

set_trap_gate(0x26,&floppy_interrupt);

每个中断向量号具体意义这里不做说明,有兴趣的同志可以参考清华大学出版社出版的《保护方式下的80386及其编程》和赵炯博士的《Linux内核完全注释》;中断调用的具体过程将在后面的例子中详细分析。现在我们关心的是初始化完毕的IDT,调试查看这张表的内容,选取0x0号、0x20号、0x80号中断作为例子。通过查看System.map文件可知:0x0号中断调用的divide_error函数地址为0x8dec,0x20号中断调用的timer_interrupt函数地址为0x74f4,0x80号中断调用的system_call函数地址为0x7418。当内核第一次调用fork函数创建进程0的子进程时,IDT表已经初始化完毕,因此我们在fork函数地址0x753c处设置断点,启动bochsdgb进行调试,命令行如下:

<bochs:1> break 0x753c

<bochs:2> c

(0) Breakpoint 1, 0x753c in ?? ()

Next at t=16879006

(0) [0x0000753c] 0008:0000753c (unk. ctxt): call .+0x93d4             ; e8931e00

00

<bochs:3> dump_cpu

……

idtr:base=0x54b8, limit=0x7ff

……

IDT基址为0x54b8,0号中断描述符的地址为0x54b8+0*8=0x54b8,20号中断描述符的地址为0x54b8+0x20*8= 0x55b8,80号中断描述符的地址为0x54b8+0x80*8=0x58b8,分别查看内存这三个地址的8字节内容,命令行如下:

<bochs:4> x /2 0x54b8

[bochs]:

0x000054b8 <bogus+       0>:    0x00088dec      0x00008f00

<bochs:5> x /2 0x55b8

[bochs]:

0x000055b8 <bogus+       0>:    0x000874f4      0x00008e00

<bochs:6> x /2 0x58b8

[bochs]:

0x000058b8 <bogus+       0>:    0x00087418      0x0000ef00

门描述符具有如下形式:


m+7


m+6


m+5


m+4


m+3


m+2


m+1


m+0


Offset(31...16)


Attributes


Selector


Offset(15...0)


Byte m+5


Byte m+4


BIT7


BIT6


BIT5


BIT4


BIT3


BIT2


BIT1


BIT0


BIT7


BIT6


BIT5


BIT4


BIT3


BIT2


BIT1


BIT0


P


DPL


DT0


TYPE


000


Dword Count

                               

因此调试信息显示,0x0号中断描述符中断调用地址为0x0008:0x00008dec,是一个特权级为0的386陷阱门,0x20号中断描述符中断调用函数地址为0x0008:0x000074f4,是一个特权级为0的386中断门,0x80号中断描述符中断调用函数地址为0x0008:0x00007418,是一个特权级为3的386陷阱门。这和预先分析的情况一致。

任务的内核态堆栈

在分析中断响应过程之前,先介绍一下任务的内核态堆栈。

当中断事件发生时,中断源向cpu发出申请,若cpu受理,则保存当前的寄存器状态、中断返回地址等许多信息,然后cpu转去执行相应的事件处理程序。中断处理完毕后,cpu将恢复之前保存的信息,并继续原来的工作。因为中断处理需要在内核态下进行,因此每个任务都有一个内核态堆栈,用来完成中断处理中保护现场和恢复现场的工作。这个内核态堆栈与每个任务的任务数据结构放在同一页面内,在创建新任务时,fork函数在任务tss内核级字段中设置,代码位于kernel/fork.c的copy_process函数中,如下:


/*p即需创建的新任务*/

p->tss.esp0 = PAGE_SIZE + (long) p;

p->tss.ss0 = 0x10;

tss.esp0和tss.ss0的值在任务内核态工作时不会被改变,因此任务每次进入内核态工作时,这个堆栈总是空的。

一般中断的处理过程

0x3号中断用于暂停程序的执行,通过查看Linux代码,可以知道对这个中断的处理仅仅是打印一些寄存器状态信息。选取这个中断作为例子的意义在于:它有一个完整的保护现场和恢复现场的过程(比如0x0号中断的处理将直接终止进程而不需要恢复现场);中断信号可以由用户态的程序产生。

0x3号中断处理程序int3在kernel/asm.s中定义,如下:


#源代码书写顺序并非如此,这样排列是为了阅读的方便

_int3:

pushl $_do_int3

jmp no_error_code

no_error_code:

#以下入栈操作为保护现场的动作

xchgl %eax,(%esp)

pushl %ebx

pushl %ecx

pushl %edx

pushl %edi

pushl %esi

pushl %ebp

push %ds

push %es

push %fs

pushl $0         # "error code"

lea 44(%esp),%edx

pushl %edx

movl $0x10,%edx

mov %dx,%ds

mov %dx,%es

mov %dx,%fs

call *%eax   #调用实际中断处理函数

addl $8,%esp

#以下出栈操作为恢复现场的动作

pop %fs

pop %es

pop %ds

popl %ebp

popl %esi

popl %edi

popl %edx

popl %ecx

popl %ebx

popl %eax

iret

这里有个问题:如果发生特权级改变,用户态的堆栈指针在什么时候保存和恢复?答案是cpu响应中断时自动将这些数据入栈,执行iret指令时自动将这些数据出栈。下面的实验可以验证这一点。

接下来的试验比较繁琐,按照以下步骤进行:

1.  编写产生0x3号中断的程序。

2.  在int3函数地址处设置断点,查看此时内核态堆栈的内容,即验证保护现场的动作。

3.  执行直到中断返回,验证iret指令的作用,即验证恢复现场的动作。

编写产生0x3号中断的程序非常简单,启动bochs+linux-0.11-devel-040329(这个img由赵炯博士加入了gcc)。用vi创建编辑一个c文件int3.c,代码如下:


#include <stdio.h>

int main()

{

__asm__(“int3”);

return 0;

}

编译这个文件产生执行程序int3。

通过查看System.map文件可知0x3号中断处理函数_int3的地址为0x8e2f。启动bochsdgb进行调试,命令行如下:

<bochs:1> b 0x8e2f

<bochs:2> c   #同时在启动的Linux下运行int3程序,将获得下面这些信息

(0) Breakpoint 1, 0x8e2f in ?? ()

Next at t=143245141

(0) [0x00008e2f] 0008:00008e2f (unk. ctxt): push 0x7af4               ; 68f47a00

00

首先关注一下内核堆栈中的内容,当前任务(0x60-0x20)/8=8号任务的tss结构中的ss0和esp0字段包含了内核态堆栈的段描述符和堆栈指针,tss结构的地址由GDT表的TSS描述符提供。继续调试,命令行如下:

<bochs:3> dump_cpu

……

esp:0xfa3fec  #这个值在在后面的分析将用到

……

tr:s=0x60, dl=0x32e80068, dh=0x89fa, valid=1

gdtr:base=0x5cb8, limit=0x7ff

……

<bochs:4> x /2 0x5d18  #0x5cb8+0x60=0x5d18

[bochs]:

0x00005d18 <bogus+       0>:    0x32e80068      0x00008bfa

<bochs:5> x /26 0x00fa32e8

[bochs]:

0x00fa32e8 <bogus+       0>:    0x00000000      0x00fa4000      0x00000010

0x00000000

0x00fa32f8 <bogus+      16>:    0x00000000      0x00000000      0x00000000

0x00000000

0x00fa3308 <bogus+      32>:    0x000398af      0x00000246      0x00000000

0x00000005

0x00fa3318 <bogus+      48>:    0x000574c0      0x00000014      0x03fffdd8

0x03fffde4

0x00fa3328 <bogus+      64>:    0x00000001      0x00000000      0x00000017

0x0000000f

0x00fa3338 <bogus+      80>:    0x00000017      0x00000017      0x00000017

0x00000017

0x00fa3348 <bogus+      96>:    0x00000068      0x80000000

对这些调试信息按照tss字段的顺序排列得出下表:


BIT31—BIT16


BIT15—BIT1


BIT0


Offset


Data


0000000000000000


链接字段


0


0x00000000


ESP0


4


0x00fa4000


0000000000000000


SS0


8


0x00000010


ESP1


0CH


0x00000000


0000000000000000


SS1


10H


0x00000000


ESP2


14H


0x00000000


0000000000000000


SS2


18H


0x00000000


CR3


1CH


0x00000000


EIP


20H


0x000398af


EFLAGS


24H


0x00000246


EAX


28H


0x00000000


ECX


2CH


0x00000005


EDX


30H


0x000574c0


EBX


34H


0x00000014


ESP


38H


0x03fffdd8


EBP


3CH


0x03fffde4


ESI


40H


0x00000001


EDI


44H


0x00000000


0000000000000000


ES


48H


0x00000017


0000000000000000


CS


4CH


0x0000000f


0000000000000000


SS


50H


0x00000017


0000000000000000


DS


54H


0x00000017


0000000000000000


FS


58H


0x00000017


0000000000000000


GS


5CH


0x00000017


0000000000000000


LDTR


60H


0x00000068


I/O许可位图偏移


000000000000000


T


64H


0x80000000

表1:任务8的tss结构

由表1可知:任务8内核态堆栈的起始堆栈指针为0x00fa4000。查看寄存器状态可知当前堆栈指针指向0x00fa3fec,与栈顶相差20/4 = 5个字,调试查看这5个字的内容,命令行如下:

<bochs:6> x /5 0xfa3fec

[bochs]:

0x00fa3fec <bogus+       0>:    0x0000001c      0x0000000f      0x00010202

0x03fffefc

0x00fa3ffc <bogus+      16>:    0x00000017

这些信息就是cpu在进入int3中断处理之前自动保存的信息,参考赵炯博士的《Linux内核完全注释》可知:在用户程序(进程)将控制权交给中断处理程序之前,cpu会首先将至少12字节的信息压入中断处理程序的堆栈中。这种情况与一个长调用(段间子程序调用)比较相像。Cpu会将代码段选择符合返回地址的偏移值压入堆栈。另一个与段间调用比较相像的地方是80386将信息压入到了目的代码的堆栈上。当发生中断时,这个目的堆栈就是内核态堆栈。另外cpu还总是将标志寄存器EFLAGS的内容压入堆栈。如果优先级别发生变化,比如从用户级改变到内核系统级,cpu还会将原代码的堆栈段值和堆栈指针压入中断程序的堆栈中。

按照堆栈向下增长方向整理调试信息,如下表所示:


0x0000


原SS


0x00000017


原ESP


0x03fffefc


EFLAGS


0x00010202


0x0000


CS


0x0000000f


EIP


0x0000001c

表2:发生中断时堆栈的内容

执行iret指令返回时也类似从一个段间子程序调用的返回,堆栈中的这些内容将自动弹出到响应寄存器中,完成中断返回恢复现场的动作。调试来验证这一过程,命令行如下:

<bochs:7> n   #7,8,9指令都是为了找到iret的位置

Next at t=172477604

(0) [0x00008e34] 0008:00008e34 (unk. ctxt): jmp .+0x8df1              ; ebbb

<bochs:8> n

Next at t=172477605

(0) [0x00008df1] 0008:00008df1 (unk. ctxt): xchg dword ptr ss:[esp], eax ; 87042

4

<bochs:9> u /30

……

00008e20: (                    ): iretd                     ; cf

<bochs:10> b 0x8e20

<bochs:11> c

(0) Breakpoint 2, 0x8e20 in ?? ()

Next at t=172498467

(0) [0x00008e20] 0008:00008e20 (unk. ctxt): iretd                     ; cf

<bochs:12> n   #中断返回

Next at t=172498468

(0) [0x00fac01c] 000f:0000001c (unk. ctxt): xor eax, eax              ; 31c0

<bochs:13> dump_cpu

……

esp:0x3fffefc

eflags:0x10202

eip:0x1c

cs:s=0xf, dl=0x0, dh=0x10c0fa00, valid=1

ss:s=0x17, dl=0x3fff, dh=0x10c0f300, valid=1

……

无需解释,表2和上面寄存器状态信息即可说明问题。

系统调用的处理过程

以系统调用fork函数为例,它的定义如下:


/*摘自init/main.c*/

static inline _syscall0(int,fork)


/*摘自include/unistd.h*/

#define __NR_fork 2

/*摘自include/unistd.h*/

#define _syscall0(type,name) /

type name(void) /

{ /

long __res; /

__asm__ volatile ("int $0x80" /

: "=a" (__res) /

: "0" (__NR_##name)); /

if (__res >= 0) /

return (type) __res; /

errno = -__res; /

return -1; /

}

__NR_fork值2是系统调用中断处理的跳转表的索引,这张系统调用函数指针表定义如下:


/*摘自include/linux/sched.h*/

typedef int (*fn_ptr)();


/*摘自include/linux/sys.h*/

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,

sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,

sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,

sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,

sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,

sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,

sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,

sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,

sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,

sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,

sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,

sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,

sys_setreuid,sys_setregid };

sys_call_table[2]的值是sys_fork函数指针,这个函数的功能不是我们研究的重点,有兴趣的同志可以参考其它资料。

将宏_syscall0和__NR_fork展开:


staic inline

int fork(void)

{

long __res;

__asm__ volatile ("int $0x80"

: "=a" (__res)

: "0" (2));  /* eax的值置为2*/

if (__res >= 0)

return (int) __res;

errno = -__res;

return -1;

}

现在fork函数的功能就很清楚了:将eax的值置为2,产生0x80中断,0x80中断的中断处理函数是system_call(还记得吗?set_system_gate(0x80,&system_call))。system_call定义如下:


_system_call:

cmpl $nr_system_calls-1,%eax  #eax保存系统调用跳转函数表的索引值

ja bad_sys_call

push %ds   #保护现场

push %es

push %fs

pushl %edx

pushl %ecx            # push %ebx,%ecx,%edx as parameters

pushl %ebx            # to the system call

movl $0x10,%edx         # set up ds,es to kernel space

mov %dx,%ds

mov %dx,%es

movl $0x17,%edx         # fs points to local data space

mov %dx,%fs

call _sys_call_table(,%eax,4)  #通过系统调用跳转函数表调用相关处理程序

pushl %eax

movl _current,%eax

cmpl $0,state(%eax)             # state 当前进程未就绪则进行进程调度

jne reschedule

cmpl $0,counter(%eax)         # counter  时间片用完进行则进程调度

je reschedule

ret_from_sys_call:

movl _current,%eax             # task[0] cannot have signals

cmpl _task,%eax

je 3f

cmpw $0x0f,CS(%esp)        # was old code segment supervisor ?

jne 3f

cmpw $0x17,OLDSS(%esp)        # was stack segment = 0x17 ?

jne 3f

movl signal(%eax),%ebx

movl blocked(%eax),%ecx

notl %ecx

andl %ebx,%ecx

bsfl %ecx,%ecx

je 3f

btrl %ecx,%ebx   #有信号则调用信号处理程序

movl %ebx,signal(%eax)

incl %ecx

pushl %ecx

call _do_signal

popl %eax       #恢复现场

3:     popl %eax

popl %ebx

popl %ecx

popl %edx

pop %fs

pop %es

pop %ds

iret      #中断返回

cpu 处理0x80中断与一般中断处理过程是一样的:压入cs,eip,eflags到目标堆栈,中断返回则从堆栈中弹出这些值到相应寄存器。其中断处理函数将通过系统调用函数指针表来处理相应系统调用。这个过程就不做验证了,有兴趣的同志可以参考一般中断处理的调试过程。

eip的值

在cpu响应中断源时,压入的eip的值,中断返回将这个值弹出加载到eip,用这样的方式继续应用程序控制流。这个eip的值将根据不同的异常来确定:


类别


原因


异步/同步


返回行为


中断


来自I/O设备的信号


异步


总是返回到下一条指令


陷阱


有意的异常


同步


总是返回到下一条指令


故障


潜在可恢复的错误


同步


根据故障是否可修复决定要么重新执行当前指令,要么终止


终止


不可修复的错误


同步


不会返回

表3:异常的类别(摘自《深入理解计算机系统》)

之前分析到的0x3号中断和0x80号中断即属于“陷阱”,因此它们中断处理完毕后总是由内核态转换到用户态(通过分段机制,段寄存器加载不同的段描述符),并返回到应用程序的下一条指令。

后记

中断处理的行为和长调用(段间子程序调用)的行为颇为相似,理解长调用的处理过程即可理解中断处理过程。计算机理论中很多概念都是相通的,因此,扎实的基本功完全可以触类旁通的指导我们开发应用程序。

时间: 2024-10-10 03:27:24

linux0.11下的中断机制分析的相关文章

linux0.11内核fork实现分析(不看不知道,一看很简单)

pcDuino3下支持mmc启动,官方的Uboot是采用SPL框架实现的,因为内部的SRAM空间达到32K,我们完全可以在这32K空间内编写一个完整可用小巧的bootloader来完成引导Linux kernel的目的. 我们首先介绍下SPL框架,可以先看下<GNU ARM汇编--(十八)u-boot-采用nand_spl方式的启动方法>和<GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析>,NAND_SPL也算是SPL框架下的一种模式. 当使用Nand f

第一次作业:基于Linux0.11操作系统的进程模型分析

1.前言 本文基于Linux0.11操作系统的源代码,分析其进程模型. Linux0.11下载地址:https://zhidao.baidu.com/share/20396e17045cc4ce24058aa43a81bf7b.html 2.进程的定义 程序是一个可执行的文件,而进程(process)是一个执行中的程序实例. 进程和程序的区别: 几个进程可以并发的执行一个程序 一个进程可以顺序的执行几个程序 进程由可执行的指令代码.数据和堆栈区组成.进程中的代码和数据部分分别对应一个执行文件中的

Linux-0.11内核源码分析系列:关于线性地址,逻辑地址,物理地址的关系与区别

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */     以下所有描述基于Linux0.11内核及其所编写的年

Linux0.11内核--fork进程分析

[版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5597818.html ] 据说安卓应用里通过fork子进程的方式可以防止应用被杀,大概原理就是子进程被杀会向父进程发送信号什么的,就不深究了. 首先fork()函数它是一个系统调用,在sys.h中: extern int sys_fork (); // 创建进程. (kernel/system_call.s, 208) // 系统调用函数指针表.用于系统调用中断处理程序(int 0x80),

Linux-0.11内核源码分析系列:内存管理up_wp_page()与do_wp_page()函数分析

/* * up_wp_page()函数用于解除物理页的共享状态,同时给发生写时复制的进程提供一页新的 * 物理页,新物理页是之前共享页的数据相同的拷贝. * table_entry是共享物理页的地址的指针,即页表实际地址+表内偏移地址 */ void un_wp_page(unsigned long * table_entry) { unsigned long old_page,new_page; old_page = 0xfffff000 & *table_entry; //取得共享物理页实际

Linux-0.11内核源码分析系列:进程调度sleep_on()函数分析

</pre><pre name="code" class="cpp">/* *Author : DavidLin *Date : 2014-12-10pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-1

Linux-0.11内核源码分析系列:进程调度

/*       *Author  : DavidLin       *Date    : 2014-12-10pm       *Email   : [email protected] or [email protected]       *world   : the city of SZ, in China       *Ver     : 000.000.001       *history :     editor      time            do       *     

Linux0.11内核源码分析系列:内存管理copy_page_tables()函数分析

/*   *Author  : DavidLin   *Date    : 2014-11-22pm   *Email   : [email protected] or [email protected]   *world   : the city of SZ, in China   *Ver     : 000.000.001   *history :     editor      time            do   *          1)LinPeng       2014-11

Linux-0.11内核源码分析系列:内存管理get_empty_page()与put_page()函数分析

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */ <pre name="code" class