2.1: 抽象硬件资源(Abstracting physical resources)

当遇到操作系统时,人们可能会问的第一个问题是:为什么需要操作系统? 换句话说,可以下面的系统调用列表实现为一个库,供应用程序链接使用。在这种方案中,每个应用程序甚至可以拥有一个根据其需求量身定制的库。应用程序可以直接与硬件资源交互,并以对其应用最优的方式使用这些资源(例如,为了实现高性能或可预测的性能)。某些嵌入式设备或实时系统的操作系统就是以这种方式组织的。

System call

Description

nt fork()

Create a process, return child’s PID.

int exit(int status)

Terminate the current process; status reported to wait(). No return.

int wait(int *status)

Wait for a child to exit; exit status in *status; returns child PID.

int kill(int pid)

Terminate process PID. Returns 0, or -1 for error.

int getpid()

Return the current process’s PID.

int sleep(int n)

Pause for n clock ticks.

int exec(char *file, char *argv[])

Load a file and execute it with arguments; only returns if error.

char *sbrk(int n)

Grow process’s memory by n zero bytes. Returns start of new memory.

int open(char *file, int flags)

Open a file; flags indicate read/write; returns an fd (file descriptor).

int write(int fd, char *buf, int n)

Write n bytes from buf to file descriptor fd; returns n.

int read(int fd, char *buf, int n)

Read n bytes into buf; returns number read; or 0 if end of file.

int close(int fd)

Release open file fd.

int dup(int fd)

Return a new file descriptor referring to the same file as fd.

int pipe(int p[])

Create a pipe, put read/write file descriptors in p[0] and p[1].

int chdir(char *dir)

Change the current directory.

int mkdir(char *dir)

Create a new directory.

int mknod(char *file, int, int)

Create a device file.

int fstat(int fd, struct stat *st)

Place info about an open file into *st.

int link(char *file1, char *file2)

Create another name (file2) for the file file1.

int unlink(char *file)

Remove a file.

这种库方法的缺点是,如果同时运行多个应用程序,则这些应用程序必须表现得“良好”。例如,每个应用程序必须定期放弃 CPU,以便其他应用程序可以运行。如果所有应用程序都彼此信任且没有漏洞,这种协作式的时间共享方案可能是可以接受的。然而,通常情况下,应用程序既不彼此信任,也可能存在漏洞,因此通常需要比协作方案更强的隔离性。

为了实现强隔离性,禁止应用程序直接访问敏感的硬件资源,并将这些资源抽象为服务是很有帮助的。例如,Unix 应用程序只能通过文件系统的 openreadwriteclose 系统调用与存储资源交互,而不能直接读写磁盘。这不仅为应用程序提供了使用路径名的便利,还允许操作系统(作为接口的实现者)管理磁盘。即使隔离性不是问题,刻意进行交互(或仅仅为了避免干扰彼此的运行)的程序也可能会发现文件系统比直接使用磁盘是一种更方便的抽象。

类似地,Unix 可以在多个进程之间透明地切换硬件 CPU,并根据需要保存和恢复寄存器状态,使得应用程序无需关注并发执行分时共享细节。这种透明性允许操作系统即使在某些应用程序处于无限循环时也能共享 CPU。

另一个例子是,Unix进程使用exec来构建其内存镜像,而不是直接与物理内存交互。这允许操作系统决定将进程放置在内存中的位置;如果内存资源紧张,操作系统甚至可以将部分进程数据存储在磁盘上。此外,exec为用户提供了使用文件系统存储可执行程序镜像的便利。

Unix中,进程之间的许多交互通过文件描述符实现。文件描述符不仅抽象掉了许多细节(例如,管道或文件中的数据存储位置),它们的定义还简化了交互过程。例如,如果管道中的一个应用程序失败,内核会为管道中下一个进程生成一个“文件结束”信号。

上述的表格所列出的系统调用接口经过精心设计,既为程序员提供了便利,又确保了实现强隔离的可能性。尽管 Unix 接口不是资源抽象的唯一方法,但它已被证明是一个非常有效的方法。

相关问题

  • 1、为什么说exec系统调用构建进程的内存映像,是不是启动一个新进程一般是调用fork和exec相结合实现新进程的创建,exec的实现逻辑和过程是什么?

exec 系统调用主要是用来加载和执行指定的程序,它通过将当前进程的内存映像替换为新程序的内存映像来实现这个功能。也就是说,exec 系统调用会加载一个新的程序,并将当前进程的地址空间完全替换为该程序的地址空间,从而执行新程序代码。具体来说,exec 系统调用会做以下几件事:

  1. 清空进程的用户空间exec 会清空当前进程的堆、栈等内存区域,替换为新的程序所需的内存空间。

  2. 加载新程序:它会从指定的二进制文件(比如 ELF 文件)中加载程序代码、数据段、共享库等到内存中,并对程序的符号链接、动态库等进行解析。

  3. 更新进程的状态exec 会更新进程的寄存器、堆栈、程序计数器等,指向新程序的入口点,使得进程从新程序的起始点开始执行。

  4. 维持进程的 PID 和某些资源:执行 exec 后,进程的 PID、打开的文件描述符、信号处理等资源通常保持不变。新的程序会继承这些资源。

forkexec 的结合实现新进程的创建,forkexec 系统调用常常结合使用来创建新进程,原因在于 fork 会复制当前进程,而 exec 会替换当前进程的内存映像。

  1. fork 系统调用fork 会创建一个新的进程。新进程是当前进程的副本,包括内存、堆栈、文件描述符等资源。在调用 fork 后,父进程和子进程各自继续执行代码,且它们的内存内容是相同的。

  2. exec 系统调用:在新创建的子进程中,调用 exec 会替换子进程的内存映像,加载并执行一个新的程序。注意,exec 不会创建新进程,它是对当前进程的替换操作。换句话说,通过先调用 fork 创建子进程,再在子进程中调用 exec 加载新程序,是创建新进程并执行新程序的常见方式。

这种 forkexec 的结合使用方式使得父进程和子进程可以分开执行不同的任务,同时 exec 可以确保子进程加载新的程序,而父进程继续执行原有的程序。

exec 的实现逻辑和过程,exec 系统调用的实现大致包括以下几个步骤:

  1. 查找可执行文件exec 会根据传入的路径(或命令名)查找要加载的可执行文件。这通常涉及到文件系统的查找机制,可能需要通过 PATH 环境变量来查找程序文件。

  2. 验证文件格式:一旦找到可执行文件,exec 会验证文件格式是否正确,通常是 ELF 格式或其他平台支持的格式。文件的头部会指示程序的加载方式。

  3. 分配新的内存映像exec 会为新程序分配内存空间,包括代码段、数据段、堆、栈等。这些内存区域会根据可执行文件的要求来分配,并加载相应的内容。

  4. 加载程序内容exec 会将可执行文件的代码、数据段以及需要的共享库等加载到内存中。如果需要解析动态链接库(如 .so 文件),此时会调用动态链接器来加载和链接库。

  5. 设置新的程序计数器exec 会将进程的程序计数器(PC)设置为新程序的入口点,通常是程序的 main 函数的地址。

  6. 恢复进程状态:程序的堆栈和寄存器等会根据新程序的要求重新设置。对于那些需要的资源(如文件描述符),exec 会保留,进程继续持有原有的文件描述符等资源,除非有特殊要求。

  7. 执行新程序:完成上述操作后,进程将从新程序的入口点开始执行,原来的进程内容和代码完全被替换。

总结:
exec 系统调用构建进程的内存映像,通过加载新程序并替换当前进程的内存。
启动新进程时,通常是先调用 fork 创建子进程,然后在子进程中调用 exec 加载新程序,这样可以同时保留父进程的执行和启动新程序的能力。
exec 的实现过程中会涉及到文件查找、内存分配、程序加载等步骤,最终实现将当前进程的内存映像替换为新程序的内存映像,并从新程序的入口开始执行。

  • 2、文件描述符如何抽象硬盘文件,外设,网络等软硬件资源,达到统一接口调用?

文件描述符(File Descriptor,FD)在操作系统中作为对各种软硬件资源(如硬盘文件、外设、网络连接等)的抽象,实现了统一的接口调用。这种抽象的设计使得不同类型的资源可以使用相同的方式进行访问和操作,简化了系统的编程接口。

文件描述符的抽象机制 文件描述符的基本概念:

文件描述符是一个整数值,在操作系统内部用于标识一个打开的文件或资源。 文件描述符通常从 0 开始,0 对应标准输入(stdin),1 对应标准输出(stdout),2 对应标准错误(stderr)。当程序调用 open 或类似的系统调用打开文件时,操作系统会返回一个新的文件描述符。 通过文件描述符,应用程序可以进行读取、写入、关闭等操作。 抽象资源的统一接口: 文件描述符抽象的关键点是 抽象和统一接口,即对于不同的硬件设备和资源(如文件、网络套接字、管道、外设等),操作系统都通过相同的系统调用接口进行管理和操作。主要步骤包括:

文件系统抽象:硬盘文件在操作系统中通常被视为文件系统的一部分。文件通过路径名与文件描述符相关联。在打开文件时,操作系统通过文件系统驱动程序将文件路径映射到实际存储介质(磁盘、SSD等)上,并返回一个文件描述符。 设备驱动抽象:外设(如硬盘、打印机、USB设备等)通过设备驱动程序与操作系统交互。设备驱动通过特定的系统调用与硬件通信,操作系统将设备抽象为文件。这样,程序与设备的交互就像是与文件的交互一样,使用统一的接口(如 read、write、ioctl 等)。 网络套接字抽象:网络通信(如 TCP/IP 套接字)通过套接字接口(socket)与操作系统交互。套接字也是一种特殊的文件描述符,应用程序通过套接字描述符与网络资源进行交互,实现网络数据的读写。操作系统使用相同的 read、write 等系统调用接口对网络套接字进行操作。 如何实现统一接口调用:

I/O 系统调用:无论是文件、外设还是网络,操作系统提供了一些标准的 I/O 系统调用(如 read、write、close 等),这些调用通过文件描述符与具体的硬件或资源进行交互。例如,read(fd, buffer, size) 会根据文件描述符 fd 对应的类型执行不同的底层操作: 如果 fd 是硬盘文件描述符,read 会从磁盘读取数据。 如果 fd 是网络套接字描述符,read 会从网络中读取数据。 如果 fd 是设备描述符,read 会向设备发送请求并获取数据。 多态设计:操作系统通过多态设计在底层实现不同设备的操作,确保对外提供统一的接口。系统调用会通过文件描述符找到对应的设备驱动程序或资源管理器,后者会决定如何与硬件交互。 例子:

硬盘文件:应用程序调用 open("/path/to/file", O_RDONLY) 打开硬盘文件,操作系统分配一个文件描述符 fd,程序可以通过 read(fd, buffer, size) 从文件中读取数据。 管道:管道也是一种文件描述符。程序可以通过 pipe() 创建管道,得到两个文件描述符,一个用于写入数据,另一个用于读取数据。数据流通过管道从一个进程传递到另一个进程,管道就像一个虚拟文件,具有相同的接口。 网络套接字:调用 socket() 创建一个网络套接字,返回一个文件描述符,程序可以用 read 或 write 操作网络数据。尽管底层是通过网络协议进行通信,但接口与文件的操作是相同的。 设备驱动:例如,应用程序可以通过打开 /dev/ttyS0 来访问串口设备,返回的文件描述符允许程序读取串口数据或向串口发送数据,操作接口与文件的操作相同。 统一接口的好处 简化编程模型:

程序员无需关心具体硬件的细节,只需通过统一的接口(read、write 等)进行数据传输,简化了代码的复杂性。 不同类型的资源(文件、外设、网络)都可以通过相同的 API 进行操作,从而提高了程序的可移植性。 设备抽象:

外设、网络和文件等资源通过设备文件(如 /dev 下的文件)抽象出来,使得所有 I/O 设备都可以看作是文件,程序可以通过文件操作来访问这些设备,而不必关注底层设备的具体实现。 跨平台支持:

操作系统通过统一的文件描述符接口来管理不同平台的硬件资源,确保跨平台的一致性。程序只需要关注系统调用接口,而不必关心不同硬件平台的差异。 资源共享与重定向:

通过文件描述符,操作系统可以轻松实现资源共享和重定向。比如,进程可以通过文件描述符共享标准输入输出流,或通过 dup 系统调用实现文件描述符的重定向。

总结 文件描述符通过提供统一的接口来抽象不同的硬件和资源,操作系统通过文件描述符和设备驱动程序的协作,使得不同类型的硬件资源(如硬盘文件、外设、网络等)都能够以相同的方式进行访问和操作。这种抽象方式不仅简化了编程模型,也提高了系统的灵活性和可扩展性。