现代操作系统:概述(三)
1.6 System Calls 系统调用
系统调用是用户在用户态下直接与操作系统交互的机制,即系统向用户提供了一些对系统进行控制的函数,是用户与系统间的桥梁。
当用户执行像read()这样的系统调用时会发生什么?
我知道您不太可能直接使用read(),当您需要读取文件或键盘时,如果您使用C编程,则会使用库函数scanf(),如果您使用Java编程,则会使用Scanner类。但是,C库函数scanf()本身会发出read(),与之对应Scanner类也会发出。scanf()函数中肯定使用了read()系统调用,但它额外的维护了一个缓冲区以提升系统效能。
A Method Call and the Runtime Stack 方法调用和运行时堆栈
这里我们拿一个调用函数作为示例,当产生方法调用时运行时堆栈是如何运行的。假设函数的执行地址是从40-300,而函数的执行地址是1042-1102,对的调用发生在地址的60-61处。那显而易见的是,根据链接器的使用原理,在遇到的调用时需要跳转到的绝对地址执行对应的指令,那么在跳转之前就需要记录当前正在执行的的状态,这个示例中体现在值要如何传递至1042处,执行完后返回值回到哪里。
此时我们使用一个运行时堆栈S,不如说就把他当成一个栈结构,先进后出,后进先出。
(1) 在60处将x压入S;
(2) 在61处将返回地址62压入S并跳转至1042;
(3) 在1042处从S中取出x值然后执行计算指令;
(4) 在1101处将sin(x)的值放入寄存器1,在3-4的过程中S可能会经历多次弹出和压入操作,但是在到达1101后S中的状态和1042时一致,即S中先x后62;
(5) 在1102处弹出S中的返回地址并跳转至62;
(6) 在62处从S中取出x值,此时S回到了调用函数前的状态,同时的值被计算完成。
The read() System Call
系统调用的基本处理流程:
(1) 用户程序调用IO库函数,C语言的scanf,Java的Scanner.next;
(2) 库函数执行格式化输入并调用一个小型汇编函数;
(3) 汇编程序例程将参数移动到预定义的位置并发出一个Trap,转到内核态;
(4) 操作系统在管理员模式下运行,并执行所需的(可能复杂的)操作;
(5) 操作系统发出一个RTI(return-from-interrupt),它将系统切换回用户态并返回到汇编程序;
(6) 汇编函数将结果移动到库例程所期望的位置,然后返回到库函数;
(7) 库函数完成剩下的工作,然后返回到用户程序;
read(fd, &buf, nbytes) 这个调用从文件描述符fd指定的文件中读取最多nbytes到字符数组缓冲区。返回实际读取的字节数(例如,如果遇到文件结束符,则可能小于nbytes)。
read()系统调用的基本流程(如使用scanf库函数):
1-3:将nbytes,buf和fd依次压入堆栈;
4:调用库函数,这涉及到保存返回地址并跳转到该库函数,就像上面调用sin()一样;(此时还在用户态里)
5:依赖于机器/操作系统的操作,例如将与read()相对应的系统调用号放在一个明确定义的位置,例如特定的寄存器。这可能需要汇编语言;(简单说就是存储系统调用号)
6:Trap使控制正确地进入操作系统,并将计算机转移到特权模式。要求使用汇编语言;(此时进入内核态)
7:Envelop使用系统调用号访问指针表(中断向量表)(与5对应,为什么要存?),并查找read()的处理程序所在位置;
8:read系统调用处理程序处理请求;
9:另一个魔法指令(RTI)返回到用户模式并跳转到Trap之后的位置;
10:库函数返回(还有更多;例如,计数必须返回);
11:read()完成,此时可以使用read的返回值。
系统调用表
1.6.1 进程控制系统调用
while (true) display_prompt() 显示提示 read_command(command) 读取指令 if (fork() != 0) 创建子进程 waitpid(...) 等待子进程结束 else execve(command) 子进程执行输入的指令 endif endwhile |
fork()系统调用复制该进程。也就是说,我们现在有了第二个进程,它是实际执行fork()的进程的子进程。父母和孩子非常非常相像。例如,它们具有相同的指令,具有相同的数据,并且它们当前都在执行fork()系统调用。但这是有区别的!
fork()系统调用在子进程中返回一个0,在父进程中返回一个正整数。实际上,返回给父进程的值是子进程的PID(进程ID)。因此,父节点和子节点执行上面代码中if-then-else的不同分支。注意,简单地删除waitpid(…)可以让子进程继续(在后台),同时允许用户启动另一个作业。
#include <sys/types.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <string.h>
#define N 128
int main() { char inputCommand[N] = ""; while (1) { printf("Please Enter Your Command:"); gets(inputCommand);
if (fork() != 0) { int status; waitpid(-1, &status, 0); } else { if (strcmp(inputCommand, "ls") == 0) { char* argv[] ={"ls", "-l", NULL}; execve("/usr/bin/ls", argv, NULL); } else if (strcmp(inputCommand, "pwd") == 0) { char* argv[] ={"pwd", NULL}; execve("/usr/bin/pwd", argv, NULL); } else { printf("Invalid Command!\n"); } } } } |
1.6.2 文件管理系统调用
大多数文件都是按顺序从头到尾访问的。在本例中执行的操作是
open()——可能是创建文件
多个read()和write()
close ()
对于非顺序访问,lseek用于移动文件指针(File Pointer),该指针是文件中将发生下一次读或写的位置。
1.6.3 文件夹管理系统调用
目录由mkdir和rmdir创建和销毁。通过创建、修改和删除文件来改变目录。如前所述,打开可以创建文件。文件可以有多个名称:link为现有文件提供另一个名称,unlink则删除一个名称。当姓氏消失时(并且文件不再被任何进程打开),文件数据将被销毁。
在Unix中,一个文件系统可以挂载(mount)另一个文件系统上。完成此操作后,对第二个文件系统上现有目录的访问将被整个第一个文件系统临时替换。大多数情况下,选择的目录在挂载之前是空的,因此没有文件(暂时)不可见。
上面的图片显示了两个文件系统;第二行显示右边的文件系统挂载到/y上时的结果。在这两种情况下,方形代表目录,圆形代表普通文件。
这就是Unix系统如何使所有文件(即使是在不同物理磁盘上和使用不同文件系统的文件)成为单个根的后代。
posted on 2021-10-03 22:30 ThomasZhong 阅读(98) 评论(0) 编辑 收藏 举报