2010年(88)
分类: LINUX
2010-08-22 16:01:53
一. 系统调用的基本概念 通常,在OS的核心中都设置了一组用于实现各种系统功能的子程序(过程),并将它们提供给用户调用。每当用户在程序中需要OS提供某种服务时,便可利用一条系统调用命令,去调用系统过程。它一般运行在核心态;通过软中断进入;返回时通常需要重新调度(因此不一定直接返回到调用过程)。 系统调用是沟通用户(应用程序)和操作系统内核的桥梁。 二. Linux的系统调用 Linux系统调用的流程非常简单,它由0x80号软中断进入系统调用入口,通过使用系统调用表保存系统调用服务函数的入口地址来实现。 2.1 Linux系统调用的数据结构 在文件"arch/i386/entry.S"中定义了系统调用表(sys_call_table),该表保存了Linux的所有基于Intel x86系列体系结构的计算机的166个系统调用入口地址(其中3个保留,Linux开辟的系统调用表可容纳256项),其中每项都被说明成 long型。下面是其中几项: .data ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) /* 0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) …… …… .long SYMBOL_NAME(sys_nanosleep) /* 162 */ .long SYMBOL_NAME(sys_mremap) .long 0,0 .long SYMBOL_NAME(sys_vm86) .space (NR_syscalls-166)*4 NR_syscalls是在"include/linux/sys.h"文件中定义的宏,其值为256,表示x86微机上最多可容纳的系统调用个数。 在文件"include/asm-i386/ptrace.h"中定义了一种寄存器帧结构 struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; unsigned short ds, __dsu; unsigned short es, __esu; unsigned short fs, __fsu; unsigned short gs, __gsu; long orig_eax; long eip; unsigned short cs, __csu; long eflags; long esp; unsigned short ss, __ssu; }; 该帧结构定义了各寄存器在系统调用时保存现场的堆栈结构。 2.2 设置0x80 软中断 Linux的系统调用由0x80号软中断进入,中断向量表的初始化在系统启动时进行,各种trap入口start_kernel()函数(init/main.c)中通过调用trap_init()(arch/i386/kernel/traps.c)被设置,其中set_system_gate(0x80,&system_call)设置了0x80号软中断。 "set_system_gate()"是一个宏,它在"include/asm-i386/system.h"中被定义。调用该宏,将使addr地址值置入gate_addr中的地址值所指向的内存单元中,以上过程,使中断向量描述表中的第128项(即16进制第80项)保存了0x80号中断的中断服务程序,即system_call的入口地址。 2.3系统调用入口 在头文件"include/asm-i386/unistd.h"中,定义了一系列的与系统调用有关的宏,包括系统调用序号,如: #define __NR_setup 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 还定义了设置系统调用入口的宏,_syscallX(),其中X表示系统调用的参数个数,Linux定义的各种系统调用的参数个数不超过5个,因此,在该文件中,共定义了6个宏(_syscall0(type,name),……,_syscall5(type,name,type1,arg1,……,type5,arg5)。 下面以_syscall2()为例: #define _syscall2(type,name,type1,arg1,type2,arg2) \ type name(type1 arg1,type2 arg2) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \ if (__res >;= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ } 该宏的第一个参数是一类类型参数,它指明系统调用返回值的类型,第二个参数指明系统调用的名称。参数列表中若还有参数,typei,argi(i =1,2)分别表示第i 个参数的类型和第i个参数 该宏的主体部分是一内联汇编,在内联汇编中只有一条扩展汇编指令,即"int $0x80",该语句两个冒号后的语句设置寄存器。 其中第一个冒号后的语句指明返回参数(即__res)使用eax寄存器。 第二个冒号后面""0" (__NR_##name),"将参数name与"__NR_"串接起来,形成的标志符存入eax寄存器,作为区别系统调用类型的唯一参数,例如设置name为"clone",那么,编译时将把"__NR_"与之串接,被视为标志符"__NR_clone",由于在文件"include/asm-i386/unistd.h"中已定义其为120,那么,传给eax的值将为120。 后面的语句将参数arg1,arg2分别传给寄存器ebx和ecx,在"_syscallX"宏中,约定五个参数分别与五个寄存器对应: arg1值存入寄存器ebx; arg2值存入寄存器ecx; arg3值存入寄存器edx; arg4值存入寄存器esi; arg5值存入寄存器edi; 在该宏的最后,判断返回值"__res"是否合法,若为负数,表明在系统调用中出错,将其相反数作为出错号赋给全局变量"errno",并返回-1,否则返回__res的值。 该宏展开得到一个与系统调用同名的函数, 该函数中的内联汇编指令int $0x80使程序转入系统调用。 2.4 系统调用过程 system_call是在汇编语言文件"/arch/i386/entry.S"中定义的入口,在Linux中,所有的系统调用都是通过中断"int &0x80"语句来实现的,因而,system_call是所有系统调用的入口。 下面解释关于它的一些重要指令,以清晰它的流程: 1. 首先,调用宏过程"SAVE_ALL"保存现有通用寄存器值,寄存器值的压栈不但可以保存系统调用前个寄存器的数据,而且提供了一种传递参数的方法,堆栈中的结构,与该过程所要传递的pt_regs结构类型的参数结构一致。在该宏中,还使ds和es指向内核的数据段,使fs指向用户的数据段,使进程进入核心态。 2. 语句"cmpl $(NR_syscalls),%eax"比较NR_syscalls与eax的大小,如果eax大于或等于NR_syscalls (即256),表明指定的系统调用不合法,"jae ret_from_sys_call"使系统调用直接返回。 3. system_call接下去执行语句 movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax 该语句以 sys_call_table为基地址,eax寄存器中的内容(即系统调用的序号)乘以4为偏移量(因为long型为32位即4字节),即得到所需调用的系统调用函数的入口地址,将其存入寄存器eax。 接着判断寄存器eax值是否为0,若是,表明出错,直接返回。 4. 执行movl SYMBOL_NAME(current_set),%ebx 语句,ebx寄存器就得到了指向当前进程的指针。 movl %db6,%edx movl %edx,dbgreg6(%ebx) 以上两条语句用来保存当前调试信息,在进程控制块task_struct结构中,第8项是debugreg[8],用来指示硬件调试信息。在entry.S中,定义了一系列宏作为偏移量,用来得到当前进程的信息,它们是: state = 0 counter = 4 priority = 8 signal = 12 blocked = 16 flags = 20 dbgreg6 = 52 dbgreg7 = 56 exec_domain = 60 这样,在当前进程的task_struct结构中,保存了当前的调试信息。 5. 语句"testb $0x20,flags(%ebx)"检测当前进程是否正跟踪系统调用,如果不是 话,直接调用所选系统调用函数,执行完后直接返回。如当前进程正处于跟踪系统调用状态,则调用函数syscall_trace()(在文件"arch/i386/kernel/ptrace.c"中定义),使当前进程状态转为TASK_STOPPED,将该进程转入睡眠状态,然后从压入寄存器的堆栈中重新找到原来的eax值,再重新设置系统调用函数的偏移量,调用实现相应系统调用的函数。执行完后再次调用syscall_trace()。 6. 函数返回以后,流程进入ret_from_sys_call,该过程内处理一些系统调用返回前应该处理的事情,如检测bottom half缓冲区,判断CPU是否需要重新调度等,之后,系统调用返回。 全局变量intr_count,它虽然不是信号量,但也部分的具有了信号量的作用,它在系统处理bottom_half时增1,则其为非零,表示已有进程进入bottom_half。 语句"cmpl $0,SYMBOL_NAME(intr_count)"就是进行上述判断,若非零,处理bottom half 缓冲区。("jne handle_bottom_half")。 handle_bottom_half很简单,包含下列语句: pushl $ret_from_intr jmp SYMBOL_NAME(do_bottom_half) ALIGN 它将ret_from_init的地址压入堆栈,然后转入do_bottom_half程序中, 处理那些被激活的中断程序。然后返回。 下面两条语句判断CPU是否需要重新调度: cmpl $0,SYMBOL_NAME(need_resched) jne reschedule 其中,need_resched是一全程量,它置位,表示CPU需要重新调度,程序转向过程reschedule,先将ret_from_sys_call的地址压入堆栈,然后再跳转到进程调度程序的入口-SYMBOL_NAME(schedule),完成进程调度并将need_resched重新置零。 如果有信号(signals),则执行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()函数即可。 7. 调用宏"RESTORE_ALL"使进程离开核心态,恢复各寄存器值并返回。 三.系统调用实例分析:sys_exit 当用户发出一个退出系统命令的时候,Linux就调用系统调用sys_exit。系统调用sys_exit的主要作用是终止当前正在运行的所有用户的应用程序,保存当前帐号的各种信息,逐步退出支撑Linux操作系统运行的系统子模块和子系统。这些系统子模块和子系统是: 1. 删除当前任务的实定时器。 2. 删除信号队列(destroye semaphore arrays),释放信号撤消结构(free semaphores undo structures) 3. 清空当前任务的kerneld队列。 4. 退出内存管理。 5. 关闭打开的文件。 6. 退出文件系统。 7. 释放当前任务的所有信号(signal)。 8. 退出线程(thread)。 3.1 sys_exit的初始化 在文件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通过调用0x80号软中断带参数:系统调用号(_NR_exit,即在sys_call_table中的偏移量)和退出码(exitcode)的方法,来达到调用sys_exit的目的。 3.2 系统调用sys_exit的流程 系统调用sys_exit的处理函数定义在文件kernel/exit.c中。 sys_exit的函数体很简单,只是调用了函数do_exit: do_exit((error_code&0xff)<<; (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()。(该函数定义在kernel/sys.c中) 再该函数中,保存当前帐号的各种状态。 2. 把当前任务的标记记为退出: current->;flags |= PF_EXITING; 向所有的进程宣布,现在系统要退出了,以便一些调度处理函数得知这一消息(通过检测该标记)。 3. 删除当前的实定时器: del_timer(¤t->;real_timer); 4. 删除信号队列(destroye semaphore arrays),释放信号撤消结构(free semaphores undo structures) Sem_exit();(定义在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(); 6. 退出内存管理系统: __exit_mm(current);(函数定义在kernel/exit.c文件中) 具体做法如下: a. 将cache,tlb,page里的内容全部回写。 b. 退出内存影射。 c. 释放页表(page table)。 7. 把当前任务所打开的文件都关闭,释放文件指针。 __exit_files(current); 8. 退出文件系统: __exit_fs(current); 9. 释放当前任务的所有信号(signal): __exit_sighand(current); 10.释放当前线程数据: __exit_thread(); 11. 向外广播退出: exit_notify(); 先将当前任务的状态(state)设为TASK_ZOMBIE,退出码为code(即传进来的参数)。再调用exit_notify()函数。在exit_notify()中, 作为我们执行上述过程后,退出系统的结果,我们的进程组们应该变成孤立的了。如果它们已经停止工作了,给它们发"SIGHUP"和"SIGCONT"信号。接着,通知它们的父进程,本进程已经被kill了。 接下去是一循环,该循环主要做以下两件事: 使初始进程(init)继承所有子进程。 检查是否有遗漏:还有进程组不是孤立的。若有,处理方法同上。 最后,调用函数disassciate_ctty(int)(定义在 drivers/char/tty_io.c中)。只有当参数是1时,才是被exit_notify()调用的。在该函数中,将当前tty进程组kill掉,所有进程对应的tty成员赋NULL。 12.将当前任务的用户数目减一: (*current->;exec_domain->;use_count)--; (*current->;binfmt->;use_count)--; 13.继续调度: schedule(); 在调用完sys_exit后,先判断返回值_res是否为非负数。若是,则说明该次调用是成功的,返回_res即可。若_res为负数,则说明该次调用过程中存在着一个或一些错误,将错误值赋给一全局变量errno: errno = -_res; 再返回-1,指明有错误存在,可以让专门处理这些错误的函数根据errno的值知道这次调用到底出错在什么地方,是哪种类型的错,以便进行错误处理。 参考资料 (1) 《计算机操作系统》 汤子瀛 (2) 《操作系统讲义》 李善平 (3) Linux Kernel Hackers' Guide |