实验5:中断/异常/系统调用
姓名:
学号:
要求(对于系统调用):
1、Linux的系统调用利用了x86的哪种硬件机制?
系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户模式切换为内核模式来对它进行处理。这就是说,执行系统调用的异常指令时,将自动地将系统切换为内核模式,并安排异常处理程序的执行。它知道如何处理这一调用。
在LINUX中实现系统调用异常的实际指令是:
int $0x80
这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在系统调用时不必用机器指令编程,在标准的C语言库中为每一个系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码非常短。它要做的工作只是将送给系统调用的参数值加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回给你的程序。为了使系统调用执行成为一项简单的任务,LINUX中提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。
2、说明Linux是如何组织系统调用的?
在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。
系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。
因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。调用子程序的例子
内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.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_close)
.long SYMBOL_NAME(sys_waitpid)
… …
.long SYMBOL_NAME(sys_capget)
.long SYMBOL_NAME(sys_capset) /* 185 */
.long SYMBOL_NAME(sys_sigaltstack)
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。
call *sys_ call-table(,%eax, 4)
由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置
3、说明一个用户态的程序是如何进入系统调用中执行的?
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数
是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
4、以某个系统调用为例,从用户激活系统调用开始,分析系统调用在Linux中是如何一步一步执行的
系统调用实例分析: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)。
系统调用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。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论