原来自己多年来一直使用的库函数竟有如此复杂的机制。这个机制的设计者思考的如此深入,屏蔽了底层硬件的差异,也是费劲心思地为了安全考虑设计了内核态和用户态。
突然想到一个问题,是不是说基本上所有的操作系统都提供了类似的机制来达到我们要求的以上的特性?
如流行的Vxworks、uCos、FreeRTos?是否需要硬件提供相应的支持?
本次实验的目的是让我们了解系统调用的C实现方式以及汇编实现方式。在考虑到性能上的差异,不知道一直使用的库函数采用了哪一种方式。
另一个问题,系统调用本身因为也是函数调用,会涉及到堆栈、寄存器的保存等。但有一点不同的是,普通的函数调用栈空间是临时分配的。而系统调用则享受到了VIP的待遇,在内核设计之初,进程的4G地址空间中(32bit)的3~4G(0xC0000000-0xFFFFFFFF)就专门留给了内核代表进程执行指令的时候使用的代码、数据和栈。这样说来,其实进程的地址空间上是有两个栈的-用户栈,内核栈。
至于为什么这样设计,随着课程的深入想必会有更深的理解。
- 通过中断(int 0x80)来实现;
寄存器 eax 中存放系统调用号,同时系统调用返回值也存放在 eax 中;
当系统调用参数小于等于6个时,参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi ,ebp中;
当系统调用参数大于6个时,全部参数应该依次放在一块连续的内存区域里,同时在寄存器 ebx 中保存指向该内存区域的指针
在系统调用中,绝大多数参数都不是很多,下面以getpid为例,这个系统调用是用来获取当前进程的pid,系统调用号:20,不需要额外参数传入。
c语言版本:
int Getpid(int argc, char **argv)
{
pid_t pid;
pid = getpid();
printf("current process's pid:%d\n",pid);
return 0;
}
汇编版本:
int GetpidAsm(int argc, char **argv)
{
pid_t pid;
asm volatile(
"mov $20, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
:"=m"(pid)
);
printf("current process's pid(ASM):%d\n",pid);
return 0;
}
代码截图:
实验效果:
如果直接在本地虚拟机上编译运行的话,更加直观了,代码如下:
#include <stdio.h>
#include <string.h>
#include "unistd.h"
int Getpid(void)
{
pid_t pid;
pid = getpid();
printf("current process's pid:%d\n",pid);
return 0;
}
int GetpidAsm(void)
{
pid_t pid;
asm volatile(
"mov $20, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
:"=m"(pid)
);
int main()
{
unsigned char c;
do
{
printf("getpid:pls input 1 to call C edition or 2 to call asm edition.\n");
c = getchar();
}
while(c <'1'|| c > '2' );
switch(c)
{
case '1':
Getpid();
break;
case '2':
GetpidAsm();
break;
default:
break;
}
}
结果如下:
在这个系统调用的实现中,唯一需要传递的参数便是系统调用号:20,不需要其他参数的传递,比较简单。
用户模式和内核模式,是为了使操作系统内核提供一个无懈可击的进程抽象,限制了一个应用可以执行的指令以及它可以访问的地址空间范围。
运行应用程序代码的进程初始时是在用户模式中的。系统调用发生时,控制传递到相应的异常处理例程,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回用户模式。