系统调用

系统调用门

由于引入了特权级机制,处于ring0的os内核代码不能被处于ring3的用户进程代码直接访问。用户进程想要获得os内核的功能支持需要通过系统调用(syscall)。应用程序可以通过调用门接口,实现在低特权级的情况下对os提供的接口函数进行调用。调用门由调用门描述符实现,其结构如下图所示:
系统调用门
16-31位代表被调用函数所在的代码段,0-15位代表被调用函数相对于代码段的偏移量。调用门的工作流程与权限检查可以查看intel提供的文档。

系统调用流程

下面以sleep系统调用为例来说明一下系统调用的流程。首先是传递系统调用参数的结构体

1
2
3
4
5
6
7
8
typedef struct _syscall_args_t {
int id; //系统调用号
int arg0; //最多支持四个参数
int arg1;
int arg2;
int arg3;
}syscall_args_t;

系统调用号定义在syscall.h,目前的支持如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define SYS_msleep              0
#define SYS_getpid 1
#define SYS_fork 2
#define SYS_execve 3
#define SYS_yield 4
#define SYS_exit 5
#define SYS_wait 6

#define SYS_open 50
#define SYS_read 51
#define SYS_write 52
#define SYS_close 53
#define SYS_lseek 54
#define SYS_isatty 55
#define SYS_sbrk 56
#define SYS_fstat 57
#define SYS_dup 58
#define SYS_ioctl 59

#define SYS_opendir 60
#define SYS_readdir 61
#define SYS_closedir 62
#define SYS_unlink 63


#define SYS_printmsg 100

msleep的syscall函数实现如下

1
2
3
4
5
6
7
8
9
10
int msleep (int ms) {
if (ms <= 0) {
return 0;
}

syscall_args_t args;
args.id = SYS_msleep;
args.arg0 = ms;
return sys_call(&args);
}

首先在函数内部定义了一个系统调用参数结构体,传入id和参数之后调用sys_call这个函数,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline int sys_call (syscall_args_t * args) {
const unsigned long sys_gate_addr[] = {0, SELECTOR_SYSCALL | 0}; // 使用特权级0
int ret;
__asm__ __volatile__(
"push %[arg3]\n\t"
"push %[arg2]\n\t"
"push %[arg1]\n\t"
"push %[arg0]\n\t"
"push %[id]\n\t"
"lcalll *(%[gate])\n\n"
:"=a"(ret)
:[arg3]"r"(args->arg3), [arg2]"r"(args->arg2), [arg1]"r"(args->arg1),
[arg0]"r"(args->arg0), [id]"r"(args->id),
[gate]"r"(sys_gate_addr));
return ret;
}

首先定义了一个数组变量sys_gate_addr,传递了系统调用门描述符的地址,然后将参数、系统调用号、门描述符地址压栈后调用lcalll指令进行调用。调用lcalll后cpu会自动进行特权级检查,获取syscall代码段的代码和偏移量,即找到do_handler_syscall函数(其GDT表项在init时进行初始化和装填),实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void do_handler_syscall (syscall_frame_t * frame) {
// 检查边界
if (frame->func_id < sizeof(sys_table) / sizeof(sys_table[0])) {
// 查表取得系统调用处理函数
syscall_handler_t handler = sys_table[frame->func_id];
if (handler) {
int ret = handler(frame->arg0, frame->arg1, frame->arg2, frame->arg3);
frame->eax = ret; // 设置系统调用的返回值,由eax传递
return;
}
}
// 不支持的系统调用则打印出错信息
task_t * task = task_current();
log_printf("task: %s, Unknown syscall: %d", task->name, frame->func_id);
frame->eax = -1; // 设置系统调用的返回值,由eax传递
}

syscall_frame_t存储了系统调用需要的栈信息,结构为

1
2
3
4
5
6
7
8
typedef struct _syscall_frame_t {
int eflags;
int gs, fs, es, ds;
int edi, esi, ebp, dummy, ebx, edx, ecx, eax;
int eip, cs;
int func_id, arg0, arg1, arg2, arg3;
int esp, ss;
}syscall_frame_t;

这时候就可以通过系统调用号查找sys_table,sys_table相当于一个函数指针表,每一项都指向了一个syscall的处理函数。找到对应的处理函数时就可以让操作系统处理对应的功能,达到请求操作系统服务的效果。例如msleep对应的内核函数为sys_mslepp,其实现方式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sys_msleep (uint32_t ms) {
// 至少延时1个tick
if (ms < OS_TICK_MS) {
ms = OS_TICK_MS;
}

irq_state_t state = irq_enter_protection();

// 从就绪队列移除,加入睡眠队列
task_set_block(task_manager.curr_task);
task_set_sleep(task_manager.curr_task, (ms + (OS_TICK_MS - 1))/ OS_TICK_MS);

// 进行一次调度
task_dispatch();

irq_leave_protection(state);
}

fork与exec系统调用