我们都知道操作系统主要是管理着计算机的硬件和软件资源,为用户态进程与硬件设备进行交互提供了一组接口——系统调用。这为应用程序开发人员提供良好的环境来使得应用层序具更好的兼容性。
系统调用的原理
操作系统中的状态分为管态(核心态)和目态(用户态)。特权指令:一类只能在核心态下运行而不能在用户态下运行的特殊指令。用户程序只在用户态下运行,有时需要访问系统核心功能,这时通过系统调用接口使用系统调用。
在现在CPU中都有几种不同的指令级别。在高执行级别下代码可以执行特权指令,访问任意的物理地址。而在相应的低级别执行状态下,代码的掌控范围会受到相应的限制。在intel x86 CPU有四种不同的执行级别0-3。而在linux下只使用其中的0级和3级分别表示内核态和用户态。
API和系统调用
应用编程接口(application program interface, API)和系统调用是不同的。
- API只是一个函数定义
- 系统调用通过软中断向内核发出一个明确的请求
Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)。
- 一般每个系统调用对应一个封装例程
- 库再用这些封装例程定义出给用户的API
- 不是每个API都对应一个特定的系统调用。
- API可能直接提供用户态的服务
- 一个单独的API可能调用几个系统调用
- 不同的API可能调用了同一个系统调用
请求系统调用
为了方便讨论和分析。下面我们使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用。在这里选出我们系统调用号为20的系统调用sys_getpid(),该系统调用用于返回当前进程的进程号数。系统调用列表参见http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl
下面是直接使用库函数API使用系统调用,getpid.c代码如下:
#include <stdio.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid=getpid();
printf("The number of current process is: %d\n",pid);
return 0;
}
下面再使用C语言内嵌汇编代码的方式使用同一个系统调用。getpid_asm.c代码如下:
#include <stdio.h>
#include<unistd.h>
int main()
{
pid_t pid;
asm volatile(
"mov $0,%%ebx\n\t"
"mov $0x14,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
:"=m" (pid)
);
printf("The number of current process is: %d\n",pid);
return 0;
}
两种方式使用同一个系统调用如上面两个代码所示,我们分别用下面命令编译两个.c文件。
gcc getpid.c -o getpid -m32
gcc getpid_asm.c -o getpid_asm -m32
因为我的是64位系统,所以在输出后面加上-m32生成32位的执行文件。运行结果如下图。
系统调用代码分析
下面我们就分别对上述两段代码进行分析。
代码一:
问题一:getpid()是什么?在哪里声明?在哪里实现?
- getpid()是一个POSIX标准的API,用于用户程序从用户态进入到内核态,在内核态读取当前进程的(tack_struct)的pid,然后返回给用户态的程序。
- getpid()函数在/usr/include/unistd.h里面声明,所以在我的头文件就包含了。
- getpid()函数在glibc函数库里面实现。gcc 编译程序时,会到glibc函数库里面寻找getpid()的实现代码,然后编译。
问题二: getpid()是怎么样进入内核态的?
当我们要清楚函数getpid()是glibc对系统调用sys_getpid的封装,用于获取当前进程的进程号的时候一切就迎刃而解了。sys_getpid系统调用号为20.在用户态时候,如果用户调用了getpid(),系统会产生一中断,进入到了内核态执行sys_getpid。getpid()的功能是返回当前进程的ID,它本身是不能完成的,必须请求操作系统服务即sys_getpid,让操作系统把当前进程的ID告诉给getpid()。所以应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系我们可以用下图大概表示。
代码二:
内核态在代码二中用汇编就清楚的表示出来了。下面我们来分析。代码二是c代码中嵌入汇编,这方面的知识我在我的博文《操作系统是如何工作的?——简单多道程序内核代码内核分析》中有详解,这里就不多说。上述代码二中有几处代码难点:
"mov $0x14,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
“mov $0x14,%%eax\n\t”这句是干什么用的呢,仔细一看十六进制0x14代表的十进制数就是20,将20放入eax寄存器里面。放到里面有什么用呢?其实吧这里涉及到系统调用的传参的问题,内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数。传递参数用哪个寄存器呢?系统默认的是eax寄存器,所以这句意思就是传递系统调用号。
那么下一句”int $0x80\n\t”又是干啥的呢?其实这是这段内嵌汇编使用软中断汇编指令int引发0x80号中断,进入为系统调用设置的中断门。这时候系统就进入了内核态,执行系统会调用。
再下一句”mov %%eax,%0\n\t”这就是系统内核执行完之后通过eax返回。eax的值就是我们需要的值,将他存入“%0”中,也就是pid中。
总的来说getpid()是在glibc里面实现的。实现过程大概入下:
getpid()
{
往exa寄存器存入__NR_getpid(系统调用号);
int 0x80,产生软中断。
…….
内核执行
……
从堆栈里面读取ID返回。
}
内核是如何为getpid()提供服务的?详细过程如下图所示。
总结
当用户态进程调用一个系统调用时,linux产生一个中断,CPU切换到内核态并开始执行一个内核函数。内核函数将返回值存放到eax寄存器,然后再通过用户的定义的变量返回。