You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/* Clone the calling process, creating an exact copy. Return -1 for errors, 0 to the new process, and the process ID of the new process to the old process. */extern__pid_tfork (void) __THROWNL;
在现代操作系统里,由于系统资源可能同时被多个应用程序访问,如果不加保护,那各个应用程序之间可能会产生冲突,对于恶意应用程序更可能导致系统奔溃。这里所说的系统资源包括文件、网络、各种硬件设备等。比如要操作文件必须借助操作系统提供的api(比如linux下的fopen)。
系统调用在我们工作中无时无刻不打着交道,那系统调用的原理是什么呢?在其过程中做了哪些事情呢?
本文将阐述系统调用原理,让大家对于系统调用有一个清晰的认识。
更多文章见个人博客:https://github.com/farmerjohngit/myblog
概述
现代cpu通常有多种特权级别,一般来说特权级总共有4个,编号从Ring 0(最高特权)到Ring 3(最低特权),在Linux上之用到Ring 0和RIng 3,用户态对应Ring 3,内核态对应Ring 0。
普通应用程序运行在用户态下,其诸多操作都受到限制,比如改变特权级别、访问硬件等。特权高的代码能将自己降至低等级的级别,但反之则是不行的。而系统调用是运行在内核态的,那么运行在用户态的应用程序如何运行内核态的代码呢?操作系统一般是通过中断来从用户态切换到内核态的。学过操作系统课程的同学对中断这个词肯定都不陌生。
中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中有一个叫中断向量表的数组来映射这个关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。
中断分为硬件中断和软件中断,我们这里说的是软件中断,软件中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表你调用第0x80号的中断处理程序。
中断号是有限的,所有不会用一个中断来对应一个系统调用(系统调用有很多)。Linux下用int 0x80触发所有的系统调用,那如何区分不同的调用呢?对于每个系统调用都有一个系统调用号,在触发中断之前,会将系统调用号放入到一个固定的寄存器,0x80对应的中断处理程序会读取该寄存器的值,然后决定执行哪个系统调用的代码。
在Linux2.5(具体版本不是很确定)之前的版本,是使用int 0x80这样的方式实现系统调用的,但其实int指令这样的形式性能不太好,原因如下(出自这篇文章):
正是由于如此,在linux2.5开始支持一种新的系统调用,其基于Intel 奔腾2代处理器就开始支持的一组专门针对系统调用的指令
sysenter
/sysexit
。sysenter
指令用于由 Ring3 进入 Ring0,sysexit
指令用于由 Ring0 返回 Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比 INT n/IRET 快了不少。本文分析的是int指令,新型的系统调用机制可以参见下面几篇文章:
https://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/index.html
https://www.jianshu.com/p/f4c04cf8e406
基于int的系统调用
触发中断
我们以系统调用
fork
为例,fork函数的定义在glibc(2.17版本)的unistd.h
fork
函数的实现代码比较难找,在nptl\sysdeps\unix\sysv\linux\fork.c
中有这么一段代码其作用简单的说就是将
__libc_fork
当作__fork
的别名,所以fork函数的实现是在__libc_fork
中,核心代码如下我们分析定义了
ARCH_FORK
的情况,ARCH_FORK
定义在nptl\sysdeps\unix\sysv\linux\i386\fork.c
中,代码如下:INLINE_SYSCALL代码在
sysdeps\unix\sysv\linux\i386\sysdep.h
INLINE_SYSCALL
主要是调用同文件下的INTERNAL_SYSCALL
这里是一段内联汇编代码, 其中
__NR_##name
的值为__NR_clone
即120。这里主要是两个步骤:int $0x80
陷入中断int $0x80
指令会让cpu陷入中断,执行对应的0x80中断处理函数。不过在这之前,cpu还需要进行栈切换。因为在linux中,用户态和内核态使用的是不同的栈(可以看看这篇文章),两者负责各自的函数调用,互不干扰。在执行
int $0x80
时,程序需要由用户态切换到内核态,所以程序当前栈也要从用户栈切换到内核栈。与之对应,当中断程序执行结束返回时,当前栈要从内核栈切换回用户栈。这里说的当前栈指的就是ESP寄存器的值所指向的栈。ESP的值位于用户栈的范围,那程序的当前栈就是用户栈,反之亦然。此外寄存器SS的值指向当前栈所在的页。因此,将用户栈切换到内核栈的过程是:
反之,从内核栈切换回用户栈的过程:恢复ESP、SS等寄存器的值,也就是用保存在内核栈的原ESP、SS等值设置回对应寄存器。
中断处理程序
在切换到内核栈之后,就开始执行中断向量表的
0x80
号中断处理程序。中断处理程序除了系统调用(0x80
)还有如除0异常(0x00
)、缺页异常(0x14
)等等,在arch\i386\kernel\traps.c
文件的trap_init
方法中描述了中断处理程序向中断向量表注册的过程:SYSCALL_VECTOR
定义如下:所以
0x80
对应的处理程序就是system_call
这个方法,该方法位于arch\i386\kernel\entry.S
主要分为几步:
1.保存各种寄存器
2.根据系统调用号执行对应的系统调用程序,将返回结果存入到eax中
3.恢复各种寄存器
其中保存各种寄存器的
SAVE_ALL
定义在entry.S中:sys_call_table
定义在entry.S中:sys_call_table
就是系统调用表,每一个long元素(4字节)都是一个系统调用地址,所以*sys_call_table(,%eax,4)
的含义就是sys_call_table
上偏移量为0+%eax*4
元素所指向的系统调用,即第%eax
个系统调用。上文中fork
系统调用最终设置到eax的值是120,那最终执行的就是sys_clone
这个函数,注意其实现和第2个系统调用sys_fork
基本一样,只是参数不同,关于fork和clone的区别可以看这里,代码如下:一次系统调用的基本过程已经分析完,剩下的具体处理逻辑和本文无关就不分析了,有兴趣的同学可以自己看看。
整体调用流程图如下:
End
想写这篇文章的原因主要是年前在看《《程序员的自我修养》》这本书,之前对于系统调用这块有一些了解但很零碎和模糊,看完本书系统调用这一章后消除了我许多疑问。总体来说这是一本不错的书,但我相关的基础比较薄弱,所以收获不多。
The text was updated successfully, but these errors were encountered: