- [ ] 一些值得问的问题解决了吗: 查询到2.11
1.Overview
1.1 Goal:
- 了解操作系统(O/S)的设计和实现
- 有扩展小型O/S的实践经验
- 有编写系统软件的实际经验
1.2 O/S的目标:
- 在许多应用程序中复用硬件
- 隔离应用程序以包含错误
- 允许在协作应用程序之间共享
- 安全控制共享
- 不要妨碍高表现
- 支持广泛的应用
1.3 组成:分层图
- user applications: vi, gcc, DB, &c
- kernel services
- h/w: CPU, RAM, disk, net, &c
我们非常关心接口和内核内部结构
1.4 O/S内核通常提供的服务:
- 进程(正在运行的程序)
- 内存分配
- 文件内容
- 文件名、目录
- 访问控制(安全)
- 其他:用户、IPC、网络、时间、终端
1.5 应用程序-内核的接口:system call
1 | fd = open("out", 1); |
1.6 O/S设计实现既困难又有趣:
- 无情的环境:古怪的h/w,难以调试
- 高效vs抽象/可移植/通用
- 功能强大vs简单的界面
- 灵活与安全
- 交互功能:
fd = open(); fork()
- 用途多种多样:笔记本电脑、智能手机、云计算、虚拟机、嵌入式
- 不断发展的硬件:NVRAM、多核、快速网络
2.UNIX系统调用简介
2.1 应用程序通过系统调用看到O/S;这个界面将是一个大焦点
2.2 展示一些示例,并在xv6上运行它们
xv6具有与UNIX系统(如Linux)类似的结构。
但是要简单得多——能够消化所有的xv6
- 开源,文档齐全,设计简洁,广泛使用
- 如果您需要了解Linux内部,学习xv6将会有所帮助
- 核心功能示例:虚拟内存、多核、中断等
2.3 例子:copy.c, 将输入复制到输出
1 | copy |
从输入中读取字节,并将其写入输出
- read()和write()是系统调用
- read()/write()参数1:是一个“文件描述符”(fd)
- read()参数2:是指向要读取的内存的指针
- read()参数3:是要读取的最大字节数
- read()可以少读,但不能多读
- 返回值:实际读取的字节数,或者-1表示错误
fd是什么:
- 传递给内核,告诉它读/写哪个“打开的文件”
- 一定是之前打开过的
- FD连接到文件/设备/套接字等
- 一个进程可以打开很多文件,有很多fd
- UNIX约定:fd 0为“标准输入”,1为“标准输出”
- FD是一个小整数
- FD索引到由内核维护的每个进程表中
- 不同的进程有不同的FD命名空间
2.4 例子:open.c,创建一个文件
1 | open |
Open()创建一个文件,返回一个文件描述符(或-1表示错误)
2.5 当程序调用像open()这样的系统调用时会发生什么?
- 看起来像一个函数调用,但实际上是一个特殊指令
- 硬件保存了一些用户寄存器
- 硬件提高特权级别
- 硬件跳转到内核中已知的“入口点”
2.6 输入UNIX的命令行界面shell。
- shell打印“$”提示符
- shell允许您运行UNIX命令行实用程序
- UNIX也支持其他类型的交互:window systems, GUIs, servers, routers, &c
- 我们可以通过shell执行许多系统调用
2.7 例子:fork.c 创建一个新进程
shell为键入的每个命令创建一个新进程
1 | echo hello |
fork()系统调用创建一个新进程
1 | fork |
内核生成一个调用进程的副本
- 指令、数据、寄存器、文件描述符、当前目录
- 产生“父”进程和“子”进程
- 父进程与子进程唯一的区别是:fork()在parent中返回一个pid,在child中返回0
- fork.c的"fork() returned"在两个进程中执行,所以“if(pid == 0)”允许代码进行区分
pid(进程ID):一个整数,内核给每个进程一个不同的pid
2.8 例子:exec.c 用可执行文件替换调用进程
shell是如何运行程序的:
1 | echo a b c |
- 程序存储在文件中:指令和初始存储器,由编译器和链接器创建
- 有一个名为echo的文件,包含指令
1 | exec |
exec()用可执行文件替换当前进程
- 丢弃指令和数据存储器
- 从文件加载指令和内存
- 保留文件描述符
exec(filename, argument-array)
- argument-array保存命令行参数;exec传递给main()
2.9 例子:forkexec.c ,Fork()一个新进程,exec()一个程序
1 | forkexec |
forkexec.c包含一个常见的UNIX习语:
- fork()子进程
- exec()子进程中的命令
- 父进程wait()s子进程完成
shell会为您输入的每个命令fork/exec/wait:
- 在wait()之后,shell打印下一个提示符
- 要在后台运行 – & – shell跳过wait()
Exit (status) -> wait(&status):
- 状态约定:0 =成功,1 =命令遇到错误
2.10 例子:redirect.c ,重定向命令的输出
1 | echo hello > out |
fork, change FD 1 in child, exec echo
1 | redirect |
open()总是选择最低的未使用FD;1由于关闭(1)
fork、fd和exec很好地相互作用以实现I/O重定向
- 在执行之前,给child一个改变fd的机会
- fd提供了间接指导:命令只使用fd 0和fd 1,不需要知道它们的位置
- exec保留sh设置的fd
- 只有sh需要知道I/O重定向,而不是每个程序
2.11 一些值得问的问题
- Why these I/O and process abstractions? Why not something else?
- Why provide a file system? Why not let programs use the disk their own way?
- Why FDs? Why not pass a filename to write()?
- Why are files streams of bytes, not disk blocks or formatted records?
- Why not combine fork() and exec()?
2.12 例子:pipe1.c 通过pipe通信
shell是如何实现的
1 | ls | grep x |
1 | pipe1 |
FD可以引用“pipe”,也可以引用文件
pipe()系统调用创建两个fd
- 从第一个FD读取
- 写入第二个FD
内核为每个管道维护一个缓冲区
- write()追加到缓冲区
- read()等待,直到有数据
2.13 例子:pipe2.c 进程间通信
pipe很好地与fork()结合来实现ls / grep x
- shell创建pipe
- forks(twice)
- 将ls的FD 1连接到pipe的write FD,并将fd0设置为管道
2.14 list.c 列出目录中的文件
– ⌚️03:06 —