什么是操作系统的接口

什么是操作系统的接口
既然使用者是通过操作系统接口来使用计算机的那到底是什么是操作系统提供的接口呢接口interface这个词来源于电气工程学科指的是插座与插头的连接口起到将电与电器连接起为的功能。后来延伸到软件工程里指软件包向外提供的功能模块的函数接口。所以接口是用来连接两个东西、信号转换和屏蔽细节。那对于操作系统来说操作系统通过接口的方式建立了用户与计算机硬件的沟通方式。用户通过调用操作系统的接口来使用计算机的各种计算服务。为了用户友好性操作系统一般会提供两个重要的接口来满足用户的一些一般性的使用需求命令行实际是一个叫bash/sh的端终程序提供的功能该程序底层的实质还是调用一些操作系统提供的函数。窗口界面窗口界面通过编写的窗口程序接收来自操作系统消息队列的一些鼠标、键盘动作进而做出一些响应。对于非一般性使用需求操作系统提供了一系列的函数调用给软件开发者由软件开发者来实现一些用户需要的功能。这些函数调用由于是操作系统内核提供的为了有别于一般的函数调用被称为系统调用。比如我们使用C语言进行软件开发时经常用的printf函数它的内部实际就是通过write这个系统调用让操作系统内核为我们把字符打印在屏幕上的。为了规范操作系统提供的系统调用IEEE制定了一个标准接口族被称为POSIXPortable Operating System Interface of Unix。一些我们熟悉的接口比如fork、pthread_create、open等。2. 用户模式与内核模式计算机硬件资源都是操作系统内核进行管理的那我们可以直接用内核中的一些功能模块来操作硬件资源吗可以直接访问内核中维护的一些数据结构吗 当然不行有人会说为什么不行呢我买的电脑内核代码在内存中那内存不都是我自己买的吗我自己不能访问吗现在我们运行的操作系统都是一个多任务、多用户的操作系统。如果每个用户进程都可以随便访问操作系统内核的模块改变状态那整个操作系统的稳定性、安全性都大大降低了。为了将内核程序与用户程序隔离开在硬件层面上提供了一次机制将程序执行的状态分为了不同的级别从0到3数字越小访问级别越高。0代表内核态在该特权级别下所有内存上的数据都是可见的可访问的。3代表用户态在这个特权级下程序只能访问一部分的内存区域只能执行一些限定的指令。操作系统在建立GTD表的时候将GTD的每个表项中的2位4种特权级别设置为特权位DPL然后操作系统将整个内存分为不同的段不同的段在GDT对应的表项中的DPL位是不同的。比如内核内存段的所有特权位都为00。而用户程序访存时在保护模式下都是通过段寄存器IP寄存器来访问的而段寄存器里则用两位表示当前进程的级别CPL是位于内核态还是用户态。既然如此那我们还有什么办法可以调用操作系统的内核代码呢操作系统为了实现系统调用提供了一个主动进入内核的惟一方式中断指令int。int指令会将GDT表中的DPL改为3让我们可以访问内核中的函数。所以所有的系统调用都必须通过调用int指令来实现大致的过程如下用户程序中包含一段包含int指令的代码操作系统写中断处理获取相调程序的编号操作系统根据编号执行相应的代码3. 剖析printf函数下面我们以printf函数的调用为例说明该函数是如何一步一步最终落在内核函数上去的。图1应用程序、库函数和内核系统调用之间的关系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 (__res0) \ 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 (__res0) return (type) __res; errno-__res; return -1; }我们看到实际函数内部并没有做太多的事情主要就是调用int 0x80将把相关的参数传递给一些通用寄存器调用的结果通过eax返回。其中一个很重要的调用参数是__NR_write这个也是一个宏就是wirte的系统调用号在linux/include/unistd.h中被定义为4同样还有很多其他系统调用号。因为所有的系统调用都是通过int 0x80那怎么知道具体需要什么功能呢只能通过系统调用号来识别。下面我们来看看int 0x80是如何执行的。这是一个系统中断操作系统对于中断处理流程一般为关中断CPU关闭中段响应即不再接受其它外部中断请求保存断点将发生中断处的指令地址压入堆栈以使中断处理完后能正确地返回。识别中断源CPU识别中断的来源确定中断类型号从而找到相应的中断服务程序的入口地址。保护现场所将发生中断处理有关寄存器中断服务程序中要使用的寄存器以及标志寄存器的内存压入堆栈。执行中断服务程序转到中断服务程序入口开始执行可在适当时刻重新开放中断以便允许响应较高优先级的外部中断。恢复现场并返回把“保护现场”时压入堆栈的信息弹回原寄存器然后执行中断返回指令IRET从而返回主程序继续运行。前3项通常由处理中断的硬件电路完成后3项通常由软件中断服务程序完成。图2系统调用中断处理流程那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(dpl13)(type8))), \ 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_calls72 # 最大的系统调用个数 .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(最后位是特权级)所以段号为4内核态数据段 mov %dx,%es movl $0x17,%edx # 把fs的段标号设置为0001 0111(最后位是特权级)所以段号为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就是所有系统调用的函数指针数组。