麻省理工大学XV6操作系统赏析(用户态的实用工具)

1.    从系统调用走起

         XV6操作系统的编译及bochs或Qemu运行环境的搭建,MIT的网站上介绍得很详细了,就不再重复。不过,MIT网上下载的dot-bochsrc里对物理内存的只配置了32M,而Xv6源代码Memlayout.h里的物理内存却把宏PHYSTOP定义成了0xE000000,即224M物理内存。所以用make bochs来运行时,会出错。把dot-bochsrc里的megs: 改为256即可.

柿子拣软的捏。让我们从Xv6的用户态应用程序开始吧。不少的操作系统的分析从

CPU复位开始走起,这是一个很好的思路,抓住了最源头,但要从源头走起,需要一开始就对相应CPU体系结构的较好掌握,与硬件的寄存器直接打交道,毕竟属于低级或底层操作,逻辑性未必有应用层那么强,但这一关终究是要走,只是这里我们先放一放,让我们换一个思路,从大部分程序员最熟悉的用户态搞起,先品品XV6操作系统提供的用户态实用工具的源代码,如cat,wc, shell,grep ,ls。毕竟,严格地说,一个完整的操作系统包括了运行在CPU管态的Kernel和运行在CPU目态的实用工具Utilities。如果是微内核的架构,更多的功能都会运行在目态。现代的CPU都支持不同运行级别的,管理者要用更高的权限,被管理者的权限自然要更低。操作系统内核要为所欲为,当然要处在管理的状态,即管态或称核心态;而应用态的用户程序是被管理者,自然要处在目态或称用户态。两者的差别在于,处于目态时,有些CPU的特权指令是不能执行的,显然一个用户程序不用执行“关中断”这样的指令。这个道理,就如网站上,只有管理员同志才能看谁不爽就删掉谁的贴,一般用户没有这样大的权力。

  让我们先假设XV6的内核已经写好,要让程序员能在上面做应用层的开发,Kernel就必须提供一些接口让用户态的程序可以调用。站在应用层程序员的角度来看,可以把内核看成一个功能强大的函数库。因为这些服务是由操作系统提供的,所以称为系统调用(System Call)。站在C语言程序员的角度看,这些系统调用与普通的函数调用在使用上没什么区别。但如果到了汇编语言这一级别,两者是完全不同的。系统调用本质上是由软件触发的一次硬件中断,在X86上用的是int指令(或sysenter指令),涉及到由CPU的低权限状态切换成高权限状态, CPU硬件对之的响应与收到定时器的中断信号时的响应并无太大区别,只是系统调用来自于CPU本身,而定时器却来自于CPU之外(如果一定要较真的话,确实有很多32位的单片机的CPU芯片里就包含了大量的定时器,但逻辑上,定时器的中断仍可看成是外部中断)。而应用层的普通函数调用用的却是call指令,未涉及CPU的权限切换。实际上,我们通常只说Windows API,即较少去提Windows的系统调用。Windows提供的API有些是系统调用,有些仅是普通的函数调用。站在C程序员的角度,只在乎函数名、函数的参数、函数的返回值和函数的功能,至于到底是普通函数调用和系统调用,已经“老虎、老鼠,傻傻分不清楚”,从使用的角度来看,确实也没有必要分得那么清楚,人有时难得糊涂。但愿意去读读Kernel的我们,还是要知道这两者的区分。之后,就可以糊里糊涂地使用内核提供的服务了。

         让我们先看看Xv6提供了哪些系统调用,打开user.h,映入眼帘的是这么几个函数。

/*3*/        //system calls

/*4*/        intfork(void);

/*5*/        intexit(void) __attribute__((noreturn));

/*6*/        intwait(void);

/*7*/        intpipe(int*);

/*8*/        intwrite(int, void*, int);

/*9*/        intread(int, void*, int);

/*10*/      intclose(int);

/*11*/      intkill(int);

/*12*/      intexec(char*, char**);

/*13*/      intopen(char*, int);

/*14*/      intmknod(char*, short, short);

/*15*/      intunlink(char*);

/*16*/      intfstat(int fd, struct stat*);

/*17*/      intlink(char*, char*);

/*18*/      intmkdir(char*);

/*19*/      intchdir(char*);

/*20*/      intdup(int);

/*21*/      intgetpid(void);

/*22*/      char*sbrk(int);

/*23*/      intsleep(int);

/*24*/      intuptime(void);

         没错,这就是Xv6提供的为数不多的几个系统调用,但确实已经相当完备,麻雀虽小,五脏俱全呀。MIT XV6的教学网站提供了一份英文版的book-rev7.pdf,其中Chapter 0.  Operating SystemInterface比较详细地这些系统调用的功能,熟悉Unix或Linux的兄弟们对这几个函数应如老友重逢般激动。是的,XV6就是Unix V6在X86平台上的重生。这里班门弄斧地简介这几个系统调用的功能:

 

/*3*/        //system calls

/*4*/        intfork(void);            创建一个新进程

/*5*/        intexit(void) __attribute__((noreturn));  进程结束

/*6*/        intwait(void);           等待子进程结束

/*7*/        intpipe(int*);            创建一个管道

/*8*/        intwrite(int, void*, int);           写文件

/*9*/        intread(int, void*, int);             读文件

/*10*/      intclose(int);                               关闭文件

/*11*/      intkill(int);                                    杀死一个进程

/*12*/      intexec(char*, char**);           在当前进程地址空间中加载一个新的可执行程序

/*13*/      intopen(char*, int);                   打开一个文件

/*14*/      intmknod(char*, short, short);       创建一个设备文件

/*15*/      intunlink(char*);                                  断开文件名与对应i节点的链接,当i节点的链接数为0时,要删除相应文件

/*16*/      intfstat(int fd, struct stat*);            获取文件的相关信息

/*17*/      intlink(char*, char*);                         硬连接,不同文件名,对应同一个i节点

/*18*/      intmkdir(char*);                                  创建目录

/*19*/      intchdir(char*);                                   改变进程的当前目录

/*20*/      intdup(int);                                           复制文件描述符,用于IO重定向

/*21*/      intgetpid(void);                                    获取当前进程的进程标志符

/*22*/      char*sbrk(int);                                     扩充进程的地址空间大小(用于实现malloc)

/*23*/      intsleep(int);                                        进程睡眠

/*24*/      intuptime(void);                                  获取开机以来的系统运行时间

这几个系统调用的语义大部分可在Linux平台上调用相似的函数来领会,可找《Unix环境高级编程》这样的重量级牛刀来处理之,就不再累述。这里我们更关注这些系统调用是如何与

X86的int指令关联起来的,系统调用的这个细节平时被Linux的C标准库给遮挡,XV6上没有实现完整的C标准库,简洁的东东,自然看得更清楚。

         凭着我们的嗅觉,我们很快就会想到,这东东大概要直接调用汇编指令来实现,或者在C语言里嵌入汇编来实现。在Xv6的目录下找一找,很快我们就会找到usys.S这个文件,

/*4*/        #defineSYSCALL(name) \

/*5*/          .globl name; \

/*6*/          name: \

/*7*/            movl $SYS_ ## name, %eax; \

/*8*/            int $T_SYSCALL; \

/*9*/            ret

/*10*/     

/*11*/      SYSCALL(fork)

/*12*/      SYSCALL(exit)

/*13*/      SYSCALL(wait)

/*14*/      SYSCALL(pipe)

/*15*/      SYSCALL(read)

         以宏SYSCALL(wait)为例,展开后就是

.globl wait;

wait:

movl $SYS_wait, %eax;

int $T_SYSCALL;

ret

         再看syscall.h,我们很快就查到#defineSYS_wait    3

         再看traps.h,我们又查到#defineT_SYSCALL       64      // system call

         进一步替换后,我们就有

         Wait:

                   Movl$3,%eax                    //系统调用号  system call number

                   Int$64                                 //系统调用中断号

                   Ret                                        //wait函数返回

         此处汇编里的标号wait:可看成一个函数名来用的,就是有一个名为wait的函数。XV6内核显然提供了N个函数来供应用层的程序调用,所以得有个参数来通知内核,我们要调用的是内核中的哪一个函数,这里通过movl $3,%eax把“要调用内核中的哪一个函数”的信息存到了寄存器eax中。接下来的事情就是要用int $64来触发一个硬件中断了,CPU可以收到各种中断,自然就要对不同的中断进行编号,选用哪个中断号来作系统调用,则是由操作系统的开发者指定的。Linux用的是int 0x80;而我们的XV6选用的是int 64. 此时,热爱C语言的我们一定觉得还少了点什么,没错,站在C程序员的角度看,写一个文件的系统调用应如下所示:

         intwrite(int fd, void* buf, int n);

         那么,参数fd、buf和n放哪里去了?实际上,按照C函数的调用约定,如果我们作如下调用 write(fd, buf, n); C编译器会产生如下代码(我们用伪代码表示吧,领会大意即可):

         push  n;

         push  buf;

         push  fd;

         call   write;

         前面三个push,把三个参数从右向左依次压入用户栈,之后就调用在usys.S汇编代码中实现的函数write。真正的触发系统调用的那条中断指令是在usys.S中实现的。当然,我们也自然会想到,为了提供速度,如果把系统调用的参数都先放到寄存器里,那我们陷入内核后,取参数的动作就会更迅速些,事实上Linux就是这么干的,当然X86平台上的寄存器个数有限,如果系统调用的参数个数太多,Linux 也只好把放不下的东东存到内存栈中了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值