Chinaunix首页 | 论坛 | 博客
  • 博客访问: 345519
  • 博文数量: 88
  • 博客积分: 2011
  • 博客等级: 大尉
  • 技术积分: 885
  • 用 户 组: 普通用户
  • 注册时间: 2010-05-21 14:50
文章分类

全部博文(88)

文章存档

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

阅读(1581) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~