Skip to content

Latest commit

 

History

History
288 lines (125 loc) · 15.4 KB

操作系统概念.md

File metadata and controls

288 lines (125 loc) · 15.4 KB

0.操作系统历史

0.1 只是一些库

0.2设置权限 user mode->kernel mode 通过权限模式的设置来阻止程序越过系统访问不该访问的Rom

0.3解决并发问题

0.4目前Now

1.操作系统介绍

1.1.1 冯·诺依曼(Von Neumann)计算模型的基本概念:

​ 执行指令。处理器从内存中获取(fetch) 一条指令,对其进行解码(decode)(弄清楚这是哪条指令),然后执行(execute)它(做它应该做的事情,如两个数相加、访问内存、检查条件、跳转到函数等)。完成这条指令后, 处理器继续执行下一条指令,依此类推,直到程序最终完成。

1.1.2 操作系统的由来

​ 实际上,有一类软件负责让程序运行变得容易(甚至允许你同时运行多个程序),允许程序共享内存,让程序能够与设备交互,以及其他类似的有趣的工作。这些软件称为操作系统(Operating System,OS),因为它们负责确保系统既易于使用又正确高效地运行

​ 要做到这一点,操作系统主要利用一种通用的技术,我们称之为虚拟化(virtualization)。 也就是说,操作系统将物理(physical)资源(如处理器、内存或磁盘)转换为更通用、更强大且更易于使用的虚拟形式。因此,我们有时将操作系统称为虚拟机(virtual machine)

​ 当然,为了让用户可以告诉操作系统做什么,从而利用虚拟机的功能(如运行程序、分配内存或访问文件),操作系统还提供了一些接口(API)供你调用。实际上,典型的操 作系统会提供几百个系统调用(system call),让应用程序调用。由于操作系统提供这些调用来运行程序、访问内存和设备,甚进行其他相关操作,我们有时也会说操作系统为应用程序提供了一个标准库(standard library)

​ 最后,因为虚拟化让许多程序运行(从而共享CPU),让许多程序可以同时访问自己的指令和数据(从而共享内存),让许多程序访问设备(从而共享磁盘等),所以操作系统有时被称为资源操理器(resource manager)。每个CPU、内存和磁盘都是系统的资源(resource), 因此操作系统扮演的主要角色就是操理(manage)这些资源,以做到高效或公平,或者实 际上考虑其他许多可能的目标。为了更好地理解操作系统的角色,我们来看一些例子。

1.2 CPU的虚拟化

image-20240129161424105

输入./cpu A & ./cpu B & ./cpu C & ./cpu D &

​ 同时执行4个程序(OS提供的假象!):

image-20240129170553455

你会发现他们的顺序改变了,这也是虚拟化导致的结果。

1.3内存的虚拟化

image-20240129171012999

image-20240129171032016

​ 这两个进程使用了同一个内存地址却没有发生冲突。

​ 每个进程访问自己的私有虚拟地址空间(virtual address space)(有时称为地址空间,address space),操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。但实际情况是,物理内存是由操作系统操理的共享资源。所有这些是如何完成的,也是本书第 1 部分的主题,属于虚拟化(virtualization)的主题。

1.4并发问题

image-20240129171242610

image-20240129171515105

​ 事实证明,这些奇怪的、不寻常的结果与指令如何执行有关,指令每次执行一条。遗憾的是,上面的程序中的关键部分是增加共享计数器的地方,它需要 3 条指令:一条将计数器的值从内存加载到寄存器,一条将其递增,一条将其保存回内存。因为这3条指令并不是以原子方式(atomically)执行(所有的指令一一性执行)的,所以奇怪的事情会发生。关于这种并发(concurrency)问题,我们将在本书的第 2 部分中详细讨论。

1.5持久性

​ 在系统内存中,数据容易丢失,因为像 DRAM 这样的设备以易失(volatile)的方式存储数值。如果断电或系统崩溃,那么内存中的所有数据都会丢失。因此,我们需要硬件和软件来持久地(persistently)存储数据。这样的存储对于所有系统都很重要,因为用户非常关心他们的数据。

​ 操作系统中操理磁盘的软件通常称为文件系统(file system)。因此它负责以可靠和高效 的方式,将用户创建的任何文件(file)存储在系统的磁盘上

image-20240129172419258

​ 不像操作系统为 CPU 和内存提供的抽象,操作系统不会为每个应用程序创建专用的虚拟磁盘。相反,它假设用户经常需要共享(share)文件中的信息。例如,在编写 C 程序时, 你可能首先使用编辑器(例如 Emacs①)来创建和编辑 C 文件(emacs -nw main.c)。之后, 你可以使用编译器将源现码转换为可执行文件(例如,gcc -o main main.c)。再之后,你可以运行新的可执行文件(例如./main)。因此,你可以看到文件如何在不同的进程之间共享。 首先,Emacs 创建一个文件,作为编译器的输入。编译器使用该输入文件创建一个新的可执 行文件(可选一门编译器课程来了解细节)。最后,运行新的可执行文件。这样一个新的程序就诞生了!

​ open write close 都是系统调用(system call),执行时会转到称为文件系统(file system)的操作系统部分,然后该系统处理这些请求,并向用户返回某种错误现码。

1.总结

OS目标:1.高性能 2.OS和应用程序之间提供保护 3.可靠性

2.抽象:进程

​ 定义:运行中程序

​ 机器状态构成:内存+寄存器

2.1.1 进程API

create

destroy

wait

miscellaneous control (其他控制 例如暂停)

status

2.1.2 程序如何创建进程

​ 1.将代码和静态数据Load至内存

​ 程序最初是在Rom上,我们需要读取磁盘上的这些字节,并将其放在内存的某处

​ 对于早期的程序,Load越早越好,在运行前完成。但现代操作系统Lazy的执行该过程。程序执行期间要加载时才去加载(涉及分页和交换机制)。

​ 2.分配Stack For variable,parameter,ret-address,Heap(堆)。起初堆会很小,随着程序运行,通过 malloc()库 API 请求更多内存,操作系统可能会参与分配更多内存给进程,以满足这些调用。所有的这些内存都是由OS负责分配并提供给进程。操作系统也可能会用参数初始化栈。具体来说,它会将参数填入 main()函数,即 argc 和 argv 数组。

​ 3.其他的一些初始化任务,特别是与输入/输出(I/O)相关的任务。例如, 在 UNIX 系统中,默认情况下每个进程都有 3 个打开的文件描述符(file descriptor),用于标 准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。在 本书的第 3 部分关于持久性(persistence)的知识中,我们将详细了解 I/O、文件描述符等。

​ 4.启动程序,在入口处运行,即 main()。通过跳转到 main()例程(第 5 章讨论的专门机制),OS 将 CPU 的控制权转移到新创建的进程中,从而程序开始执行。

2.1.3进程状态

(1)基本状态:运行、就绪、阻塞

image-20240129224418464

​ 在这个简单的例子中,操作系统必须做出许多决定。首先,系统必须决定在 Process0 发出 I/O 时运行 Process1。这样做可以通过保持 CPU 繁忙来提高资源利用率。其次,当 I/O 完成时,系统决定不切换回 Process0。目前还不清楚这是不是一个很好的决定。你怎么看?这些类型的决策由操作系统调度程序完成,这是我们在未来几章讨论的主题。

2.1.4 进程数据结构

​ 为了追踪进程信息,操作系统可能会为所有就绪的进程保留某种进程列表(process list),以及跟踪当前正在运行的进程的一些附加信息。操作系统还必须以某种方式跟踪被阻塞的进程。当 I/O 事件完成时,操作系统应确保唤醒正确的进程,让它准备好再次运行。

下面的是XV6内核的数据结构

the registers xv6 will save and restore to stop and subsequently restart a process

struct context { int eip; int esp; int ebx; int ecx; int edx; int esi; int edi; int ebp; };

enum proc_state { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

the information xv6 tracks about each process including its register context and state

struct proc

{ char *mem; // Start of process memory

uint sz; // Size of process memory

char *kstack; // Bottom of kernel stack for this process

enum proc_state state; // Process state

int pid; // Process ID

struct proc *parent; // Parent process

void *chan; // If non-zero, sleeping on chan

int killed; // If non-zero, have been killed

struct file *ofile[NOFILE]; // Open files

struct inode *cwd; // Current directory

struct context context; // Switch here to run process

struct trapframe *tf; // Trap frame for the current interrupt

};

​ 除了运行、就绪和阻塞之外,还有其他一些进程可以处于的状态。有时候系统会有一个初始(initial)状态,表示进程在创建时处于的状态。另外,一个进程可以处于已退出但尚未清理的最终(final)状态(在基于 UNIX 的系统中,这称为僵尸状态)。这个最终状态非常有用,因为它允许其他进程(通常是创建进程的父进程)检查进程的返回代码,并查看刚刚完成的进程是否成功执行(通常,在基于 UNIX 的系统中,程 序成功完成任务时返回零,否则返回非零).完成后,父进程将进行最后一次调用(例如, wait()),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构

3.插叙:进程API

3.1fork()

`
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(int argc,char* argv[])
{
    printf("hello world (pid:%d)\n",(int)getpid());
    int rc=fork();
    if(rc<0)
    {
        fprintf(stderr,"fork failed\n");
        exit(1);
	}
else if(rc==0)
{
    printf("hello,i am child (pid:%d)\n",(int)getpid());

}
else
{
     printf("hello, I am parent of %d (pid:%d)\n", rc,(int)getpid());
}
return 0;}
`

fork函数的使用有点反逻辑。它一共返回两个值,一个是父进程的pid,另一个是0与-1(子进程是否创建成功)

子进程创建后不执行printf hello world语句,而是直接从fork调用中返回

子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从 fork()返回的值是不同的。父进程获得的返回值是新创建子进程的PID,而子进程获得的返回值是0。这个差别非常重要,因为这样就很容易编写代码处理两种不同的情况(像上面那样)。

另外 若在单核系统的条件下,子进程和父进程可能同时运行 照理来说应该先执行父进程再执行子进程

image-20240130220939596

但是也有可能出现先hello i am child 后出现i am parent 这就涉及到了cpu的调度问题(并发的不确定性)

3.2 wait()

image-20240130221148172

调用wait能让进程等候,确定执行顺序,更加稳定。上图调用了wait导致子线程先执行

3.3 exec()

image-20240130221443759

搭配fork

​ 给定可执行程序的名称(如wc)及需要的参数(如p3.c)后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给该进程。因此,它并没有创建新进程,而是直接将当前运行的程序(以前的p3)替换为不同的运行程序(wc)。子进程执行exec()之后,几乎就像 p3.c 从未运行过一样。对exec()的成功调用永远不会返回。

​ 当然,你的心中可能有一个大大的问号:为谁谁设计如此奇怪的接口,来完成简单的、 创建新进程的任务?好吧,事实证明,这种分离fork()及exec()的做法用构建UNIX shell的时候非常有用,因为这给了shell在fork之后exec之前运行代码的机会,这些代码可以在运行新程序前改变环境,从而让一系列有趣的功能很容易实现。

3.4 这样设计API所实现的有趣功能

image-20240130223520783

STDOUT->标准输出流 (输出到终端)

通过关闭标准输出流,再将其重定向至文件,printf语句不再打印在屏幕上,而是打印在文件中。

​ UNIX 管道也是用类似的方式实现的,但用的是pipe()系统调用。在这种情况下,一个进程的输出被链接到了一个内核管道(pipe)上(队列),另一个进程的输入也被连接到了同一个管道上。因此,前一个进程的输出无缝地作为后一个进程的输入,许多命令可以用这种方式串联在一起,共同完成某项任务。比如通过将grep、wc命令用管道连接可以完成从一个文件中查找某个词,并统计其出现次数的功能:grep -o foo file | wc -l。

3.5 其他API

​ Kill()等要求进程睡眠、终止或其他有用的指令