|
阅读:841回复:2
Linux的系统调用分析
Linux的系统调用分析
系统调用概述: 通常,在操作系统的核心中都设置了一组用于实现各种系统功能的子程序,并将它们提供给用户调用。每当用户在程序中需要操作系统提供某种服务时,便可利用一条系统调用命令,去调用系统过程。它一般运行在系统态;通过中断进入;返回时通常需要重新调度(因此不一定直接返回到调用过程)。系统调用是操作系统的一个重要组成部分,它提供了较低层次的系统调用功能,即可以被操作系统本身使用,也是一个方便的编程接口,可以实现用户直接对内核的访问。Linux系统调用的流程非常简单,它由0x80号中断进入系统调用入口,通过使用系统调用表保存系统调用服务函数的入口地址来实现。 . 系统调用向量的初始化 Startup_32()代码可以在/linux/arch/i386/kernel/head.S中找到。Startup_32()以调用setup_idt()开始。这个例程设置一中断描述表(IDT,Interrupt DescriptorTable)为一有256个入口的表。在这例程是用来设置256入口指向ignore_int,中断门。在这里,没有中断入口点被装入,它仅仅是在当页面调度被激活和并且核心被转移到内存区为0xC0000000处时被调用。一中断分配表有256个入口点,每个是4字节长,共有1024个字节。当start_kernel()(在/linux/init/main.c中)被调用时,它调用trap_init()函数(在/linux/arch/i386/kernel/traps.c中)。Trap_init()函数通过宏set_trap_gate()(在/linux/include/asm-i386/system.h中)设置中断分配表: set_call_gate(&default_ldt,lcall7); 。。。 set_trap_gate(17,&alignment_check); for (i=18;i<48;i++) set_trap_gate(i,&reserved); set_system_gate(0x80,&system_call); 。。。 其中,system_call就被设为第0x80中断。 Trap_init()初始化中断分配表如下: 0 divide_error 1 debug 2 nmi 3 int3 4 overflow 5 bounds 6 invalid_op 7 device_not_available 8 double_fault 9 coprocessor_segment_overrun 10 invalid_TSS 11 segment_not_present 12 stack_segment 13 general_protection 14 page_fault 15 spurious_interrupt_bug 16 coprocessor_error 17 alignment_check 18-48 reserved 0x80 system_call 在2.0.34版的Linux操作系统中,在系统调用表共定义了166个系统调用: ENTRY(sys_call_table) long SYMBOL_NAME(sys_setup) /* 0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) .long SYMBOL_NAME(sys_open) /* 5 */ 。。。 .long SYMBOL_NAME(sys_sched_get_priority_max) .long SYMBOL_NAME(sys_sched_get_priority_min) /* 160 */ .long SYMBOL_NAME(sys_sched_rr_get_interval) .long SYMBOL_NAME(sys_nanosleep) .long SYMBOL_NAME(sys_mremap) .long 0,0 .long SYMBOL_NAME(sys_vm86) .space (NR_syscalls-166)*4 Linux对中断和异常的使用: Linux下系统调用的执行是由一个可屏蔽中断传递的,用汇编指令表示即int 0x80(类似DOS下的int 0x21)。由0x80中断向量传递控制给内核。这一中断向量和其他重要的中断向量一样在系统启动时即被初始化。 Linux(2.0.34版本)有166个系统调用(在/usr/src/linux/arch/i386/kernel/entry.S 中)如下表: 这些系统调用的功能看括号中的英文就可知。 ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) /*No.0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) .long SYMBOL_NAME(sys_open) /*No.5 */ . . . .long SYMBOL_NAME(sys_sched_get_priority_max) .long SYMBOL_NAME(sys_sched_get_priority_min) /*No.160 */ .long SYMBOL_NAME(sys_sched_rr_get_interval) .long SYMBOL_NAME(sys_nanosleep) .long SYMBOL_NAME(sys_mremap) .long 0,0 .long SYMBOL_NAME(sys_vm86) .space (NR_syscalls-166)*4 其中SYMBOL_NAME是一个宏,在/usr/scr/Linux/linkage.h中有定义。 #ifdef __ELF__ #define SYMBOL_NAME_STR(X) #X #define SYMBOL_NAME(X) X #ifdef __STDC__ #define SYMBOL_NAME_LABEL(X) X##: #else #define SYMBOL_NAME_LABEL(X) X/**/: #endif #else #define SYMBOL_NAME_STR(X) "_"#X #ifdef __STDC__ #define SYMBOL_NAME(X) _##X #define SYMBOL_NAME_LABEL(X) _##X##: #else #define SYMBOL_NAME(X) _/**/X #define SYMBOL_NAME_LABEL(X) _/**/X/**/: #endif #endif 即SYMBOL_NAME的功能就是:针对不同的编译器,对相应的过程名或变量名做一些处理,如在标准C下,为每个过程名或变量名前面添一个下划线,以防止和系统的变量名冲突,这是相当必要的。 #define NR_syscalls 256 (见/usr/scr/linux/sys.h) NR_syscalls即系统调用的ENTRY个数。 ENTRY也是一个宏,在usr/scr//Linux/linkage.h中定义。 #define ENTRY(name) .globl SYMBOL_NAME(name); ALIGN; SYMBOL_NAME_LABEL(name) 即ENTRY声明了一个全局的符号名,并给出标号。 多余的ENTRY可以供用户添加自己的系统调用。共有256-166 = 90个。后面会介绍如何添加用户自己的系统调用或修改已有的的系统调用。 当用户执行系统调用时,执行流程如下: • 每一个系统调用都通过Lib库实现,并根据调用时参数的个数不同调用syscallx宏,x即参数的个数。(宏定义见/usr/src/linux/include/linux/unistd.h) • 下面列举了一些_syscalllx()宏定义的系统调用: • static inline _syscall0(int,idle) • static inline _syscall0(int,fork) • static inline _syscall2(int,clone,unsigned long,flags,char *,esp) • static inline _syscall0(int,pause) • static inline _syscall0(int,setup) • static inline _syscall0(int,sync) • static inline _syscall0(pid_t,setsid) • static inline _syscall3(int,write,int,fd,const char *,buf,off_t,count) • static inline _syscall1(int,dup,int,fd) • static inline _syscall3(int,execve,const char *,file,char **,argv,char **,envp) • …… • static inline _syscall3(int,open,const char *,file,int,flag,int,mode) • static inline _syscall1(int,close,int,fd) • static inline _syscall1(int,_exit,int,exitcode) • static inline _syscall3(pid_t,waitpid,pid_t,pid,int *,wait_stat,int,options) • • 当int $0x80执行后,调用才传送到核心入口指针ENTRY(system_call)。 • • 但即使是有许多参数的系统调用也是使用同一个入口点的。甚至一些更复杂的、系统调用,有可变的参数列表,仍用一样的入口指针。复杂的系统调用如 open()等。 • #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; } #define _syscall1(type,name,type1,arg1) type name(type1 arg1) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_##name),"b" ((long)(arg1))); if (__res >= 0) return (type) __res; errno = -__res; return -1; } . 它通过汇编代码段__asm__volotile调用中断"int $0x80"将参数传递给ENTRY(system_call).其中,定义res取得返回值 ,并将__NR_用##将其与name连接起来作为参数,这个参数是一个宏,定义在unistd.h中,其它参数arg1将被存放在相应的寄存器ebx之中。之所以调用中断int 0x80能跳转到ENTRY(system_call),这是因为在中断矢量表中,早已经将int 0x80中断的指针指向了系统调用system_call,这在下边的"Linux 如何初始化系统调用矢量表"中讲到的。 • 每一个系统调用的宏扩展为一个用汇编语言子程序(Linux中使用AT&T格式的汇编语言,并且支持__asm__ 开头的内嵌汇编指令)。这个子程序建好堆栈并通过int $0x80调用函数_system_call() 例如系统调用setuid _syscall1(int,setuid,uid_t,uid); 将被扩展为: _setuid: subl $4,%esp pushl %ebx movzwl 12(%esp),%eax movl %eax,4(%esp) movl $23,%eax movl 4(%esp),%ebx int $0x80 movl %eax,%edx testl %edx,%edx jge L2 negl %edx movl %edx,_errno movl $-1,%eax popl %ebx addl $4,%esp ret L2: movl %edx,%eax popl %ebx addl $4,%esp ret • 至此并没有执行任何系统代码。直到int $0x80这条指令的执行,转内核的入口点_system_call()。这个入口点对所有的系统调用都是一样。它做了保护寄存器,检查系统调用是否合法等工作后转向实际的处理代码(_sys_call_table中的偏移量)。最后它还调用_ret_from_sys_call()。 • 当系统调用结束后,_ret_from_sys_call()被调用,它检查调度器(scheduler)是否要调用。 ret_from_sys_call: movl SYMBOL_NAME(bh_mask),%eax andl SYMBOL_NAME(bh_active),%eax jne handle_bottom_half 它通过Bottom_half队列检查进程调度程序是否应该执行,如果有,就马上跳转至handle_bottom_half去处理。否则,进入ret_with_reschedule ret_with_reschedule完成如下任务: ret_with_reschedule: cmpl $0,SYMBOL_NAME(need_resched) jne reschedule ;(功能见下面) cmpl $0,sigpending(%ebx) jne signal_return RESTORE_ALL ALIGN 判断是否需要进程调度,如果需要,则跳转到reschedule去完成调度工作。如果不需要,则判断是否有消息需要处理,如果是这样的话,将跳转到signal_return去(下面将会谈到),用来处理消息并返回。如果不需要消息处理,那么就使用宏RESTORE_ALL宏来恢复所有寄存器值。 reschedule的工作: pushl $ret_from_sys_call jmp SYMBOL_NAME(schedule) 如果需要调度,先将ret_from_sys_call的地址压入堆栈,然后再跳转到进程调度程序的入口--SYMBOL_NAME(schedule),完成进程调度。 signal_return将做如下工作: signal_return: /*检查是否是虚拟页面存储管理模式*/ testl $(VM_MASK),EFLAGS(%esp) pushl %esp jne v86_signal_return /*如果不是,直接处理消息,并恢复寄存器值返回*/ pushl $0 call SYMBOL_NAME(do_signal) addl $8,%esp RESTORE_ALL ALIGN 另外,如果是虚拟页面存储管理模式,那就转入v86_signal_return中, 调用save_v86_state保存页面状态,再处理消息。 当ret_from_sys_call做完以后,ret_from_intr被调用, ret_from_intr: GET_CURRENT(%ebx) movl EFLAGS(%esp),%eax # 结合 EFLAGS 和 CS的当前值 movb CS(%esp),%al testl $(VM_MASK | 3),%eax # 返回 VM86 模式 。 jne ret_with_reschedule RESTORE_ALL 将返回值 当系统调用返回时,syscall()宏被调用: Static inline syscallX() { ……. if (__res>=0)return (type) __res; errno=-__res; return -1; } syscallx()宏代码检查返回值的负值,如果是1的话,就把返回值的正值拷贝一份放在全局变量_errno中,这样可以被其他检查错误信息的函数调用。 注: 1.用做系统调用的参数类型有一个限制,他们的容量不能超过4个字节? 因为在执行 int 0x80 时,所有的参数都是通过寄存器传递的,而在386体系结构中,寄存器是32位的,所以,他们的容量不能超过4个字节(32位)。 2.使用CPU寄存器做参数传递的另一个限制是,可以传递的参数的数目,使用CPU寄存器做参数传递最多可以传递五个参数,所以,一共定义了六个不同的syscallX()宏。(从syscall0()到syscall5()) 四.系统调用的入口和过程: 这一部分代码是在/linux/arch/i386/kernel/head.s文件中。为了提高代码的效率,因此,Linux的这一部分的代码是用汇编语言写的。 下面,根据代码ENTRY(system_call)部分的流程来一步一步分析系统调用的入口和过程: 如果传进来系统调用号%eax比NR_syscalls(定义在文件\linux\include\linux\sys.h中:#define NR_syscalls 256)大,说明该调用号码是非法的,则 跳到ret_from_sys_call 否则 将系统调用表(sys_call_table)中的第%eax个系统调用函数入口地址赋给%eax。这里,由于地址是long型的,占四个字节,因此,在movl语句中要用: movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax 用C语言表示,即为:%eax=sys_call_table[%eax*4+0](在movl语句中,第一个”,” 前省略了”0”)。关于sys_call_table的介绍详见第二部分。 如果%eax的值为”0”,则说明在sys_call_table中,没有该系统调用的处理函数,也就是说该系统调用号码是无效的,因此 跳到ret_from_sys_call 否则 假设当前没有错误,清除标志寄存器的carry位为”0”。 接着检测当前的任务执行模式是否是调试模式:flags(%ebx)?=PF_TRACESYS 如果是PF_TRACESYS模式: 调用函数syscall_trace()(定义在/Linux/arch/i386/kernel/ptrace.c文件中),将该任务终止,退出码设为SIGTRAP,再通知父任务,本任务已被终止。重新执行系统调度函数schedule()。 执行系统调用:call *SYMBOL_NAME(sys_call_table)(,%eax,4) 保存返回值到%eax中。 再次调用函数syscall_trace() 否则 执行系统调用:call *%eax 保存返回值: movl %eax,EAX(%esp) 跳到ret_from_sys_call 下面,我们来看看在ret_from_sys_call标识部分是怎么做的: 首先,通过判断全局变量intr_count来得知系统当前是否正在处理中断。 如果intr_count=0,即系统并不在执行中断处理程序,则 如果当前有中断需要服务,bh_mask&bh_active!=0,则 去做中断服务工作:handle_bottom_half 否则 如果当前模式中VM86标记被设为”1”,即为当前是在虚拟8086模式下: 如果当前任务的代码段是KERNEL_CS的话,则 返回 否则 跳到标号1 否则 跳到标号1 否则 跳到标号1 在ret_from_sys_call部分中,除了当intr_count=0,bh_mask&bh_active=0且在虚拟8086模式下,当前任务的代码段是KERNEL_CS是要返回外,其它情况都要跳到标号1处去执行。由此看来,标号1处的代码是ret_from_sys_call的关键部分,请看: 先判断系统是否需要重新调度,need_resched是否为”0”: 如果need_resched不等于”0”,则 执行reschedule。 否则 由 #ifdef __SMP__ GET_PROCESSOR_OFFSET(%edx) movl SYMBOL_NAME(current_set)(,%edx),%ebx #else movl SYMBOL_NAME(current_set),%ebx 语句首先判断是否为多CPU结构,若是,得到当前CPU的偏移值,当前的任务指针为current_set[当前CPU的号码],否则,current_set[0]即为当前任务的指针。如果当前任务task[%edx]没有信号(signal),则 返回 否则 保存块信息,再处理信号(signal) 如果有信号(signals),则 执行signal_return。Signal_return是比较简单的一部分,它根据当前模式是否是虚拟8086模式。若是,则先调用函数save_v86_state()(定义在文件/linux/ arch/i386/kernel/vm86.c中),把当前的vm86_regs结构中的信息全部保存起来,再执行函数do_signal()(定义在文件/linux/arch/i386/kernel/signal.c中),根据各种处理信号来设置当前任务的状态。若不是虚拟8086模式,则直接执行do_siganl()函数即可。 返回 综上所述,当一用户调用一系统调用时,执行如下: 1. 每次调用指向一库里的程序。在库里的每个调用函数是一个syscallX()的宏,X是一实际程序的参数的个数。一些系统调用比其他系统调用更复杂是因为不同长度的参数链。但是,就是这些复杂的系统调用必须使用相同的入口点:它们仅仅有更多参数设置罢了。 2. 每个系统调用的宏展开成一在通过0x80中断处的指令而产生的设置调用堆栈框架并且调用_system_call()的程序流程。 3. 在这里,没有执行给此调用的系统代码。直到 int $0x80被执行,此调用被转 移到核心入口点_system_call()。这个入口点对所有的系统调用都是相同的。它负责保存所有的寄存器,检查并确认一合法的系统调用被唤醒并最终通过在_sys_call_table表里的偏移来转移控制权给实际的系统调用代码。当系统调用完成时但在返回用户的空间前它也负责调用_ret_from_sys_call()。 System_call的实际代码入口点可以在/usr/src/linux/kernel/entry.S 找到。许多系统调用的实际代码可以在/usr/src/linux/kernel/sys.c找到,其余部分可以在其它地方找到,例如在/usr/src/linux/kernel/exit.c,panic.c等等。 4. 在系统调用被执行完以后,_ret_from_sys_call()被调用。它检查看调度者是否应该被运行,如果是,则调用它。 系统调用实例:sys_exit的分析 系统调用sys_exit的简介 当用户发出一个退出系统命令的时候(例如,用户打了LOGOUT等命令),Linux就会通过Int 0x80及设置一些参数来调用系统调用sys_exit。系统调用sys_exit的主要作用是终止当前正在运行的所有用户的应用程序,保存当前帐号的各种信息,逐步退出支撑Linux操作系统运行的系统子模块和子系统。这些系统子模块和子系统是: 1. 删除当前任务的实定时器。 2. 删除信号队列(destroye semaphore arrays),释放信号撤消结构(free semaphores undo structures) 3. 清空当前任务的kerneld队列。 4. 退出内存管理。 关闭打开的文件。 5. 退出文件系统。 6. 释放当前任务的所有信号(signal)。 7. 退出线程(thread)。 系统调用sys_exit的初始化过程及调用结束后的处理 在文件/linux/include/asm-i386/unistd.h中,可以找到很多包括sys_exit在内系统调用的宏定义。sys_exit的宏定义如下: static inline _syscall1(int,_exit,int,exitcode) 使用第三部分“展开宏定义”的方法将其展开以后,它变成代码如下: int _exit(int exitcode) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_exit), "b" ((long)(exitcode))); if (__res >= 0 ) return (int) __res; errno = -__res; return -1; } 可以看出来,sys_exit是一只带一个参数exitcode的函数,该参数便是系统退出(sys_exit)的退出码。_exit通过调用int 0x80并传入系统调用号(_NR_exit,即在sys_call_table中的偏移量)和退出码(exitcode)的方法,来达到调用sys_exit的目的。 在调用完sys_exit后,先判断返回值_res是否为非负数。若是,则说明该次调用是成功的,返回_res即可。若_res为负数,则说明该次调用过程中存在着一个或一些错误,将错误值赋给一全局变量errno: errno = -_res; 再返回-1,指明有错误存在,可以让专门处理这些错误的函数根据errno的值知道这次调用到底出错在什么地方,是哪种类型的错,以便进行错误处理。 系统调用sys_exit的流程 系统调用sys_exit的处理函数定义在文件/linux/kernel/exit.c中。 Sys_exit的函数体很简单,只是调用了函数do_exit: do_exit((error_code&0xff)<<8); (error_code&0xff)<<8的作用就是将error_code的低8位移到高8位中,低8位用0填补,此数将作为参数传给函数do_exit。 下面来看看函数do_exit是怎样做的。 首先,do_exit判断表示是否有正在处理的中断服务全局变量intr_count是否为1,如果为1,表明当前还有中断正在处理,执行intr_count=0,停止处理中断。 接着,do_exit要为关闭系统,逐步退出一些运行操作系统所必须的模块。 1. 执行函数acct_process()。(该函数定义在/linux/kernel/sys.c中) 再该函数中,保存当前帐号的各种状态,见结构acct的成员:(定义在/linux/include/acct.h文件中) struct acct { char ac_comm[ACCT_COMM]; /* 帐号当前正在执行的命令*/ time_t ac_utime; /* 帐号当前的用户时间*/ time_t ac_stime; /* 帐号当前的系统时间*/ time_t ac_etime; /* 帐号的本次登录到当前的时间差*/ time_t ac_btime; /* 帐号本次开始时间 */ uid_t ac_uid; /* 帐号所对应用户的ID号*/ gid_t ac_gid; /* 帐号对应用户所属组的ID号 */ dev_t ac_tty; /* 帐号所在的终端号tty*/ char ac_flag; /* 帐号的标记,包括该用户的权限描述等 */ long ac_minflt; /* 帐号的次缺页情况 */ long ac_majflt; /* 帐号的主缺页情况*/ long ac_exitcode; /* 帐号的当前进程的退出代码*/ }; 2. 把当前任务的标记记为退出: current->flags |= PF_EXITING; 向所有的进程宣布,现在系统要退出了,以便一些调度处理函数得知这一消息(通过检测该标记)。 3. 删除当前的实定时器: del_timer(¤t->real_timer); 4. 删除信号队列(destroye semaphore arrays),释放信号撤消结构(free semaphores undo structures) Sem_exit();(定义在/linux/ipc/sem.c文件中) 在该函数中,增加调整值(semval)给信号,再释放撤消结构(free undo structures)。由于某些信号(semaphore)可能已经过时或无效了,直到信号数组(semaphore array)被删除了以后,撤消结构(undo structures)才被释放。具体做法如下: a. 如果当前进程正在睡眠状况(需要一信号(semaphore)来唤醒),将进程当前指向所需信号(semaphore)的指针置空。 b. 在当前的信号撤消链表(struct sem_undo)里查找a中提到的那信号(semaphore),找到以后,调整该信号(已在信号撤消链表中注册过的)的内容。 c. 由于有可能有一个队列的进程在等该信号,故须更新整个操作系统的数组。 5. 清空当前任务的kerneld队列: kerneld_exit(); 我们需要内”冲洗”一下所有的等待进程,以便于当那些进程结束时,do_exit函数可以减少一些独立的消息的可能性。因此,我们必须保证那些消息队列是空的,无论所有的kerneld是否已经死亡。 6. 退出内存管理系统: __exit_mm(current);(函数定义在/linux/kernel/exit.c文件中) 具体做法如下: a. 将cache,tlb,page里的内容全部回写。 b. 退出内存影射。 c. 释放页表(page table)。 7. 把当前任务所打开的文件都关闭,释放文件指针。 __exit_files(current);(函数定义在/linux/kernel/exit.c文件中) 8. 退出文件系统: __exit_fs(current);(函数定义在/linux/kernel/exit.c文件中) 9. 释放当前任务的所有信号(signal): __exit_sighand(current);(函数定义在/linux/kernel/exit.c文件中) 10.释放当前线程数据: __exit_thread(); 11。向外广播退出: exit_notify();(定义在/linux/kernel/exit.c文件中) 先将当前任务的状态(state)设为TASK_ZOMBIE,退出码为code(即传进来的参数)。再调用exit_notify()函数。在exit_notify()中, 作为我们执行上述过程后,退出系统的结果,我们的进程组们应该变成孤立的了。如果它们已经停止工作了,给它们发”SIGHUP”和”SIGCONT”信号。接着,通知它们的父亲,本进程已经被杀了。 接下去是一循环,该循环主要做以下两件事: a. 使初始进程(init)继承所有子进程。 b. 检查是否有漏网之鱼:还有进程组不是孤立的。若有,处理方法与上同。 最后,调用函数disassciate_ctty(int)(定义在 linux\drivers\char\tty_io.c 文件中)。只有当参数是1时,才是被exit_notify()调用的。在该函数中, 将当前tty进程组杀掉,所有进程对应的tty成员赋NULL。 12.将当前任务的用户数目减一: (*current->exec_domain->use_count)--; (*current->binfmt->use_count)--; 13.继续调度: schedule(); 至此,帐号的退出工作已经全部完成了sys_exit()的系统调用既已完成。 |
|
|
|
1C#
发布于:2002-10-18 19:49
Linux内核源代码的阅读和工具介绍
Linux内核源代码的阅读和工具介绍
Linux的内核源代码可以从很多途径得到。一般来讲,在安装的linux系统下,/usr/src/linux目录下的东西就是内核源代码。另外还可以从互连网上下载,解压缩后文件一般也都位于linux目录下。 许多人对于阅读Linux内核有一种恐惧感,其实大可不必。当然,象Linux内核这样大而复杂的系统代码,阅读起来确实有很多困难,但是也不象想象的那么高不可攀。只要有恒心,困难都是可以克服的。也不用担心水平不够的问题,事实上,有很多事情我们不都是从不会到会,边干边学的吗? 任何事情做起来都需要有方法和工具。正确的方法可以指导工作,良好的工具可以事半功倍。对于Linux 内核源代码的阅读也同样如此。下面我就把自己阅读内核源代码的一点经验介绍一下,最后介绍Window平台下的一种阅读工具。 对于源代码的阅读,要想比较顺利,事先最好对源代码的知识背景有一定的了解。对于linux内核源代码来讲,我认为,基本要求是:1、操作系统的基本知识;2、对C语言比较熟悉,最好要有汇编语言的知识和GNU C对标准C的扩展的知识的了解。另外在阅读之前,还应该知道Linux内核源代码的整体分布情况。我们知道现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序、网络等组成。看一下Linux内 核源代码就可看出,各个目录大致对应了这些方面。Linux内核源代码的组成如下(假设相对于linux目录): arch 这个子目录包含了此核心源代码所支持的硬件体系结构相关的核心代码。如对于X86平台就是i386。 include 这个目录包括了核心的大多数include文件。另外对于每种支持的体系结构分别有一个子目录。 init 此目录包含核心启动代码。 mm 此目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,如对应于X86的就是arch/i386/mm/fault.c 。 drivers 系统中所有的设备驱动都位于此目录中。它又进一步划分成几类设备驱动,每一种也有对应的子目录,如声卡的驱动对应于drivers/sound。 ipc 此目录包含了核心的进程间通讯代码。 modules 此目录包含已建好可动态加载的模块。 fs Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext2文件系统对应的就是ext2子目录。 kernel 主要核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。 net 核心的网络部分代码。里面的每个子目录对应于网络的一个方面。 lib 此目录包含了核心的库代码。与处理器结构相关库代码被放在arch/*/lib/目录下。 scripts此目录包含用于配置核心的脚本文件。 Documentation 此目录是一些文档,起参考作用。 清楚了源代码的结构组成后就可以着手阅读。对于阅读方法或者说顺序,有所谓的纵向与横向之分。所谓纵向就是顺着程序的执行顺序逐步进行;所谓横向,就是分模块进行。其实他们之间不是绝对的,而是经常结合在一起进行。对于Linux源代码来讲,启动的代码就可以顺着linux的启动顺序一步一步来,它的大致流程如下(以X86平台为例): ./larch/i386/boot/bootSect.S-->./larch/i386/boot/setup.S-->./larch/i386/kernel/head.S-->./init/main.c中的start_kernel()。而对于象内存管理等部分,则可以单独拿出来进行阅读分析。我的体会是:开始最好按顺序阅读启动代码,然后进行专题阅读,如进程部分,内存管理部分等。在每个功能函数内部应该一步步来。实际上这是一个反复的过程,不可能读一遍就理解。 俗话说:“工欲善其事,必先利其器”。 阅读象Linux核心代码这样的复杂程序令人望而生畏。它象一个越滚越大的雪球,阅读核心某个部分经常要用到好几个其他的相关文件,不久你将会忘记你原来在干什么。所以没有一个好的工具是不行的。由于大部分爱好者对于Window平台比较熟悉,并且还是常用Window系列平台,所以在此我介绍一个Window下的一个工具软件:Source Insight。这是一个有30天免费期的软件,可以从www.sourcedyn.com下载。安装非常简单,和别的安装一样,双击安装文件名,然后按提示进行就可以了。安装完成后,就可启动该程序。这个软件使用起来非常简单,是一个阅读源代码的好工具。它的使用简单介绍如下:先选择Project菜单下的new,新建一个工程,输入工程名,接着要求你把欲读的源代码加入(可以整个目录加)后,该软件就分析你所加的源代码。分析完后,就可以进行阅读了。对于打开的阅读文件,如果想看某一变量的定义,先把光标定位于该变量,然后点击工具条上的相应选项,该变量的定义就显示出来。对于函数的定义与实现也可以同样操作。别的功能在这里就不说了,有兴趣的朋友可以装一个Source Insight,那样你阅读源代码的效率会有很大提高的。怎么样,试试吧! -------------------- “人是一根脆弱却有思想的芦苇。” 一个人不思想,世上就多条愚汉; 一个民族不思想,就会走向疯狂。 思想是人与生俱来的权利,虽然它并不必然指向真理;但不思想却必然走向愚昧盲从。 因此剥夺人的思想权利就是一种罪。 [fly] [/fly] |
|
|
|
2C#
发布于:2002-10-18 20:34
Re:Linux的系统调用分析
学长,你来当俱乐部老大吧!!!
我是诚心的,我跟你学。 [ 2002-10-18 20:41:28 slw4qd 修改 ] |
|
[/fly]