在什么是操做系统这篇文章中,介绍过操做系统像是一个代理同样,为咱们去管理计算机的众多硬件,咱们须要计算机的一些计算服务、数据管理的服务,都由操做系统提供接口来完成。这样作的好处是让通常的计算机使用者不用关心硬件的细节。html
既然使用者是经过操做系统接口来使用计算机的,那究竟是什么是操做系统提供的接口呢?linux
接口(interface)这个词来源于电气工程学科,指的是插座与插头的链接口,起到将电与电器链接起为的功能。后来延伸到软件工程里指软件包向外提供的功能模块的函数接口。因此接口是用来链接两个东西、信号转换和屏蔽细节。数组
那对于操做系统来讲:操做系统经过接口的方式,创建了用户与计算机硬件的沟通方式。用户经过调用操做系统的接口来使用计算机的各类计算服务。为了用户友好性,操做系统通常会提供两个重要的接口来知足用户的一些通常性的使用需求:安全
bash/sh
的端终程序提供的功能,该程序底层的实质仍是调用一些操做系统提供的函数。对于非通常性使用需求,操做系统提供了一系列的函数调用给软件开发者,由软件开发者来实现一些用户须要的功能。这些函数调用因为是操做系统内核提供的,为了有别于通常的函数调用,被称为系统调用。好比咱们使用C语言进行软件开发时,常常用的printf
函数,它的内部实际就是经过write
这个系统调用,让操做系统内核为咱们把字符打印在屏幕上的。bash
为了规范操做系统提供的系统调用,IEEE制定了一个标准接口族,被称为POSIX
(Portable Operating System Interface of Unix)。一些咱们熟悉的接口好比:fork
、pthread_create
、open
等。数据结构
计算机硬件资源都是操做系统内核进行管理的,那咱们能够直接用内核中的一些功能模块来操做硬件资源吗?能够直接访问内核中维护的一些数据结构吗? 固然不行!有人会说,为何不行呢?我买的电脑,内核代码在内存中,那内存不都是我本身买的吗?,我本身不能访问吗?
如今咱们运行的操做系统都是一个多任务、多用户的操做系统。若是每一个用户进程均可以随便访问操做系统内核的模块,改变状态,那整个操做系统的稳定性、安全性都大大下降了。函数
为了将内核程序与用户程序隔离开,在硬件层面上提供了一次机制,将程序执行的状态分为了避免同的级别,从0到3,数字越小,访问级别越高。0表明内核态,在该特权级别下,全部内存上的数据都是可见的,可访问的。3表明用户态,在这个特权级下,程序只能访问一部分的内存区域,只能执行一些限定的指令。ui
操做系统在创建GTD表的时候,将GTD的每一个表项中的2位(4种特权级别)设置为特权位(DPL),而后操做系统将整个内存分为不一样的段,不一样的段,在GDT对应的表项中的DPL位是不一样的。好比内核内存段的全部特权位都为00
。而用户程序访存时,在保护模式下都是经过段寄存器+IP寄存器来访问的,而段寄存器里则用两位表示当前进程的级别(CPL),是位于内核态仍是用户态。spa
既然如此,那咱们还有什么办法能够调用操做系统的内核代码呢?操做系统为了实现系统调用,提供了一个主动进入内核的唯一方式:中断指令int
。int
指令会将GDT表中的DPL改成3,让咱们能够访问内核中的函数。因此全部的系统调用都必须经过调用int
指令来实现,大体的过程以下:操作系统
下面咱们以printf
函数的调用为例,说明该函数是如何一步一步最终落在内核函数上去的。
printf函数是C语言的一个库函数,它并非真正的系统调用,在Unix下,它是经过调用write
函数来完成功能的。
write函数内部就是调用了int
中断。通常的系统调用都是调用0x80号中断。而操做系统中通常不会的显式的写出write的实现代码,而是经过_syscall3
宏展开的实现。_syscall3
是专门用来处理有3个参数的系统调用的函数的实现。同理还有_syscall0
、_syscall1
和_syscall2
等,目前最大支持的参数个数为3个,这三个参数是经过ebx
, ecx
,edx
传递的。若是有系统调用的参数超过了3个,那么能够经过一个参数结构体来进行传递。
// linux/lib/write.c #define __LIBRARY__ #include <unistd.h> // _syscall3(int,write,int,fd,const char *,buf,off_t,count)
// linux/include/unistd.h #define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \ if (__res>=0) \ return (type) __res; \ errno=-__res; \ return -1; \ }
因此宏展开后,write函数的实现实现为:
int write(int fd, const char *buf, off_t count) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_write),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); if (__res>=0) return (type) __res; errno=-__res; return -1; }
咱们看到实际函数内部并无作太多的事情,主要就是调用int 0x80
,将把相关的参数传递给一些通用寄存器,调用的结果经过eax
返回。其中一个很重要的调用参数是__NR_write
这个也是一个宏,就是wirte的系统调用号,在linux/include/unistd.h中被定义为4,一样还有不少其余系统调用号。由于全部的系统调用都是经过int 0x80
,那怎么知道具体须要什么功能呢,只能经过系统调用号来识别。
下面咱们来看看int 0x80
是如何执行的。这是一个系统中断,操做系统对于中断处理流程通常为:
前3项一般由处理中断的硬件电路完成,后3项一般由软件(中断服务程序)完成。
那0x80号中断的处理程序是什么呢,咱们能够看一下操做系统是如何设置这个中断向量表的。在操做系统初始化时shecd_init
函数里,调用了
set_system_gate(0x80, &system_call);
咱们深刻看一下set_system_gate
函数作了什么
#define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000)) #define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)
经过上面的代码,咱们能够看出,set_system_gate
把第0x80中断表的表项中中断处理程序入口地址设置为&system_call。而且把那一项IDT表中的DPL设置了为3, 方便用户程序能够去访问这个地址。
因此init 0x80
最终会被system_call
这个函数地址处的代码来实际处理。让咱们看下system_call
作了什么事情。
# linux/kernel/system_call.s nr_system_calls=72 # 最大的系统调用个数 .globl _system_call system_call: cmpl $nr_system_calls-1,%eax # eax中放的系统调用号,在write的调用过程当中为__NR_write = 4 ja bad_sys_call push %ds # 下面是一些寄存器保护,后面还要弹出 push %es push %fs pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds # 把ds的段标号设置为0001 0000(最后2位是特权级),因此段号为4,内核态数据段 mov %dx,%es movl $0x17,%edx # 把fs的段标号设置为0001 0111(最后2位是特权级),因此段号为5,用户态数据段 mov %dx,%fs call sys_call_table(,%eax,4) # 实际的系统调用 pushl %eax movl current,%eax cmpl $0,state(%eax) # state 检测是否为就绪状态 jne reschedule # 进入调度程序 cmpl $0,counter(%eax) # counter 查看信号状态 je reschedule ret_from_sys_call: movl current,%eax # task[0] cannot have signals cmpl task,%eax je 3f cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f movl signal(%eax),%ebx movl blocked(%eax),%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call do_signal popl %eax 3: popl %eax popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret
咱们能够发现,上面代码中大部分代码是寄存器状态保存与恢复,堆栈段的切换。核心代码为call sys_call_table(,%eax,4)
,它是一个函数调用,函数的地址为sys_call_table(,%eax,4) = sys_call_table + 4*%eax
说明sys_call_table
为一个数组入口,数组中的元素长度都为4个字节,咱们要访问数组中的第%eax
个元素。而%eax
即为系统调用号。sys_call_table
就是全部系统调用的函数指针数组。
// 定义在 linux/include/linux/sys.h fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount, sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm, sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access, sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir, sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid, sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys, sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, sys_setreuid,sys_setregid };
到这里,咱们找到了最终真正的执行核心函数地址sys_write
,这个是操做实现的内核代码,全部的屏幕打印就是由该函数最终实现。它里面涉及IO的一些硬件驱动函数,咱们在这里就再也不继续深刻了。
到此,咱们已经经过printf这样一个上层的函数接口,清楚操做系统是如何一步步为了咱们提供了一个内核调用的方法。如此的精细控制,让人感叹。
下面简单说明一下,如何在操做系统源码中添加两个咱们本身的系统调用whoami
和iam
下面是具体的操做步骤。
要注意的是:在系统调用的过程当中,段寄存器ds和es指向内核数据空间,而fs被设置指向用户数据空间。所以在实际数据块信息传递过程当中Linux内核就能够利用fs寄存器来执行内核数据空间与用户数据空间之间的数据复制工做,而且在复制过程当中内核程序不须要对数据边界范围做任何检查操做。边界检查操做由CPU自动完成。内核程序的实际数据传送工做可使用get_fs_byte()
和puts_fs_bypte()
等函数进行。
[1] 《Linux内核彻底剖析基于0.12内核》 赵炯著。 [2] 网易云课堂,哈尔滨工业大学《操做系统之应用》 李治军。