本文公众号来源:java建设者
作者:cxuan
本文已收录至我的GitHub
操作系统概念大部分操作系统提供了特定的基础概念和抽象,例如进程、地址空间、文件等,它们是需要理解的核心内容。下面我们会简要介绍一些基本概念,为了说明这些概念,我们会不时的从
UNIX
操作系统一个很关键的概念就是
进程(Process)
地址空间(address space)
寄存器(registers)
程序计数器(program counter)
堆栈指针(stack pointer)
对进程建立一种直观感觉的方式是考虑建立一种多程序的系统。考虑下面这种情况:用户启动一个视频编辑程序,指示它按照某种格式转换视频,然后再去浏览网页。同时,一个检查电子邮件的后台进程被唤醒并开始运行,这样,我们目前就会有三个活动进程:视频编辑器、Web 浏览器和电子邮件接收程序。操作系统周期性的挂起一个进程然后启动运行另一个进程,这可能是由于过去一两秒钟程序用完了 CPU 分配的时间片,而 CPU 转而运行另外的程序。
像这样暂时中断进程后,下次应用程序在此启动时,必须要恢复到与中断时刻相同的状态,这在我们用户看起来是习以为常的事情,但是操作系统内部却做了巨大的事情。这就像和足球比赛一样,一场完美精彩的比赛是可以忽略裁判的存在的。这也意味着在挂起时该进程的所有信息都要被保存下来。例如,进程可能打开了多个文件进行读取。与每个文件相关联的是提供当前位置的指针(即下一个需要读取的字节或记录的编号)。当进程被挂起时,必须要保存这些指针,以便在重新启动进程后执行的
read
进程表(process table)
所以,一个挂起的进程包括:进程的地址空间(往往称作
磁芯映像
与进程管理有关的最关键的系统调用往往是决定着进程的创建和终止的系统调用。考虑一个典型的例子,有一个称为
命令解释器(command interpreter)
shell
如果一个进程能够创建一个或多个进程(称为
子进程

上图表示一个进程树的示意图,进程 A 创建了两个子进程 B 和进程 C,子进程 B 又创建了三个子进程 D、E、F。
合作完成某些作业的相关进程经常需要彼此通信来完成作业,这种通信称为
进程间通信(interprocess communication)
其他可用的进程系统调用包括:申请更多的内存(或释放不再需要的内存),等待一个子进程结束,用另一个程序覆盖该程序。
有时,需要向一个正在运行的进程传递信息,而该进程并没有等待接收信息。例如,一个进程通过网络向另一台机器上的进程发送消息进行通信。为了保证一条消息或消息的应答不丢失。发送者要求它所在的操作系统在指定的若干秒后发送一个通知,这样如果对方尚未收到确认消息就可以进行重新发送。在设定该定时器后,程序可以继续做其他工作。
在限定的时间到达后,操作系统会向进程发送一个
警告信号(alarm signal)
系统管理器授权每个进程使用一个给定的
UID(User IDentification)
GID(Group IDentification)
在 UNIX 操作系统中,有一个 UID 是
超级用户(superuser)
管理员(administrator)
每台计算机都有一些主存用来保存正在执行的程序。在一个非常简单的操作系统中,仅仅有一个应用程序运行在内存中。为了运行第二个应用程序,需要把第一个应用程序移除才能把第二个程序装入内存。
复杂一些的操作系统会允许多个应用程序同时装入内存中运行。为了防止应用程序之间相互干扰(包括操作系统),需要有某种保护机制。虽然此机制是在硬件中实现,但却是由操作系统控制的。
上述观点涉及对计算机主存的管理和保护。另一种同等重要并与存储器有关的内容是管理进程的地址空间。通常,每个进程有一些可以使用的地址集合,典型值从 0 开始直到某个最大值。一个进程可拥有的最大地址空间小于主存。在这种情况下,即使进程用完其地址空间,内存也会有足够的内存运行该进程。
但是,在许多 32 位或 64 位地址的计算机中,分别有 2^32 或 2^64 字节的地址空间。如果一个进程有比计算机拥有的主存还大的地址空间,而且该进程希望使用全部的内存,那该怎么处理?在早期的计算机中是无法处理的。但是现在有了一种
虚拟内存
几乎所有操作系统都支持的另一个关键概念就是文件系统。如前所述,操作系统的一项主要功能是屏蔽磁盘和其他 I/O 设备的细节特性,给程序员提供一个良好、清晰的独立于设备的抽象文件模型。创建文件、删除文件、读文件和写文件 都需要系统调用。在文件可以读取之前,必须先在磁盘上定位和打开文件,在文件读过之后应该关闭该文件,有关的系统调用则用于完成这类操作。
为了提供保存文件的地方,大多数个人计算机操作系统都有
目录(directory)

进程和文件层次都是以树状的结构组织,但这两种树状结构有不少不同之处。一般进程的树状结构层次不深(很少超过三层),而文件系统的树状结构要深一些,通常会到四层甚至五层。进程树层次结构是暂时的,通常最多存在几分钟,而目录层次则可能存在很长时间。进程和文件在权限保护方面也是有区别的。一般来说,父进程能控制和访问子进程,而在文件和目录中通常存在一种机制,使文件所有者之外的其他用户也能访问该文件。
目录层结构中的每一个文件都可以通过从目录的顶部即
根目录(Root directory)
路径名(path name)
/Faculty/Prof.Brown/Courses/CS101
根目录 /
在上面的系统中,每个进程会有一个
工作目录(working directory)
/Faculty/Prof.Brown
/Courses/CS101
在读写文件之前,首先需要打开文件,检查其访问权限。若权限许可,系统将返回一个小整数,称作
文件描述符(file descriptor)
在 UNIX 中,另一个重要的概念是
特殊文件(special file)
块儿特殊文件(block special file)
字符特殊文件(character special file)
/dev
还有一种与进程和文件相关的特性是管道,
管道(pipe)

如果 A 和 B 希望通过管道对话,他们必须提前设置管道。当进程 A 相对进程 B 发送数据时,它把数据写到管道上,相当于管道就是输出文件。这样,在 UNIX 中两个进程之间的通信就非常类似于普通文件的读写了。
保护计算机中含有大量的信息,用户希望能够对这些信息中有用而且重要的信息加以保护,这些信息包括电子邮件、商业计划等,管理这些信息的安全性完全依靠操作系统来保证。例如,文件提供授权用户访问。
比如 UNIX 操作系统,UNIX 操作系统通过对每个文件赋予一个 9 位二进制保护代码,对 UNIX 中的文件实现保护。该保护代码有三个位子段,一个用于所有者,一个用于与所有者同组(用户被系统管理员划分成组)的其他成员,一个用于其他人。每个字段中有一位用于读访问,一位用于写访问,一位用于执行访问。这些位就是著名的
rwx位
rwxr-x--x
操作系统是执行系统调用的代码。编辑器、编译器、汇编程序、链接程序、使用程序以及命令解释符等,尽管非常重要,非常有用,但是它们确实不是操作系统的组成部分。下面我们着重介绍一下 UNIX 下的命令提示符,也就是
shell
shell 有许多种,例如 sh、csh、ksh 以及 bash等,它们都支持下面这些功能,最早起的 shell 可以追溯到 sh
用户登录时,会同时启动一个 shell,它以终端作为标准输入和标准输出。首先显示
提示符(prompt)
美元符号($)
<pre class="brush:php;toolbar:false;">date
shell 会创建一个子进程,并运行 date 做为子进程。在该子进程运行期间,shell 将等待它结束。在子进程完成时,shell 会显示提示符并等待下一行输入。
用户可以将标准输出重定向到一个文件中,例如
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">date > file
同样的,也可以将标准输入作为重定向
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">sort <file1> file2
这会调用 sort 程序来接收 file1 的内容并把结果输出到 file2。
可以将一个应用程序的输出通过管道作为另一个程序的输入,因此有
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">cat file1 file2 file3 | sort > /dev/lp
这会调用 cat 应用程序来合并三个文件,将其结果输送到 sort 程序中并按照字典进行排序。sort 应用程序又被重定向到 /dev/lp ,显然这是一个打印操作。
系统调用我们已经可以看到操作系统提供了两种功能:为用户提供应用程序抽象和管理计算机资源。对于大部分在应用程序和操作系统之间的交互主要是应用程序的抽象,例如创建、写入、读取和删除文件。计算机的资源管理对用户来说基本上是透明的。因此,用户程序和操作系统之间的接口主要是处理抽象。为了真正理解操作系统的行为,我们必须仔细的分析这个接口。
多数现代操作系统都有功能相同但是细节不同的系统调用,引发操作系统的调用依赖于计算机自身的机制,而且必须用汇编代码表达。任何单 CPU 计算机一次执行执行一条指令。如果一个进程在用户态下运行用户程序,例如从文件中读取数据。那么如果想要把控制权交给操作系统控制,那么必须执行一个异常指令或者系统调用指令。操作系统紧接着需要参数检查找出所需要的调用进程。操作系统紧接着进行参数检查找出所需要的调用进程。然后执行系统调用,把控制权移交给系统调用下面的指令。大致来说,系统调用就像是执行了一个特殊的过程调用,但是只有系统调用能够进入内核态而过程调用则不能进入内核态。
为了能够了解具体的调用过程,下面我们以
read
<pre class="brush:php;toolbar:false;">count = read(fd,buffer,nbytes);
系统调用在 count 中返回实际读出的字节数。这个值通常与 nbytes 相同,但也可能更小。比如在读过程中遇到了文件尾的情况。
如果系统调用不能执行,不管是因为无效的参数还是磁盘错误,count 的值都会被置成 -1,然后在全局变量
errno
系统调用是通过一系列的步骤实现的,为了更清楚的说明这个概念,我们还以 read 调用为例,在准备系统调用前,首先会把参数压入堆栈,如下所示

C 和 C++ 编译器使用逆序(必须把第一个参数赋值给 printf(格式字符串),放在堆栈的顶部)。第一个参数和第三个参数都是值调用,但是第二个参数通过引用传递,即传递的是缓冲区的地址(由 & 指示),而不是缓冲的内容。然后是 C 调用系统库的 read 函数,这也是第四步。
在由汇编语言写成的库过程中,一般把系统调用的编号放在操作系统所期望的地方,如寄存器(第五步)。然后执行一个
TRAP
TRAP 指令与过程调用指令存在两个方面的不同
TRAP 指令会改变操作系统的状态,由用户态切换到内核态,而过程调用不改变模式其次,TRAP 指令不能跳转到任意地址上。根据机器的体系结构,要么跳转到一个单固定地址上,或者指令中有一 8 位长的字段,它给定了内存中一张表格的索引,这张表格中含有跳转地址,然后跳转到指定地址上。跟随在 TRAP 指令后的内核代码开始检查系统调用编号,然后
dispatch
堆栈指针(increment stackpointer)
在上面的第九步中我们说道,控制可能返回 TRAP 指令后面的指令,把控制权再移交给调用者这个过程中,系统调用会发生阻塞,从而避免应用程序继续执行。这么做是有原因的。例如,如果试图读键盘,此时并没有任何输入,那么调用者就必须被阻塞。在这种情形下,操作系统会检查是否有其他可以运行的进程。这样,当有用户输入 时候,进程会提醒操作系统,然后返回第 9 步继续运行。
下面,我们会列出一些常用的
POSIX
进程管理

文件管理

目录和文件系统管理

其他

上面的系统调用参数中有一些公共部分,例如 pid 系统进程 id,fd 是文件描述符,n 是字节数,position 是在文件中的偏移量、seconds 是流逝时间。
从宏观角度上看,这些系统调所提供的服务确定了多数操作系统应该具有的功能,下面分别来对不同的系统调用进行解释
用于进程管理的系统调用在 UNIX 中,
fork
进程标识符(Process IDentified,PID)

在多数情况下, 在 fork 之后,子进程需要执行和父进程不一样的代码。从终端读取命令,创建一个子进程,等待子进程执行命令,当子进程结束后再读取下一个输入的指令。为了等待子进程完成,父进程需要执行
waitpid
statloc
那么 shell 该如何使用 fork 呢?在键入一条命令后,shell 会调用 fork 命令创建一个新的进程。这个子进程会执行用户的指令。通过使用
execve
<pre class="brush:php;toolbar:false;">#define TRUE 1while(TRUE){/* 一直循环下去 */type_prompt(); /* 在屏幕上显示提示符 */read_command(command,parameters) /* 从终端读取输入 */if(fork() != 0){ /* fork 子进程 *//* 父代码 */waitpid(-1, &status, 0);/* 等待子进程执行完毕 */}else{/* 子代码 */execve(command,parameters,0)/* 执行命令 */}}一般情况下,execve 有三个参数:将要执行的文件名称,一个指向变量数组的指针,以及一个指向环境数组的指针。这里对这些参数做一个简要的说明。
先看一个 shell 指令
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">cp file1 file2
此命令把 file1 复制到 file2 文件中,在 shell 执行 fork 之后,子进程定位并执行文件拷贝,并将源文件和目标文件的名称传递给它。
cp 的主程序(以及包含其他大多数 C 程序的主程序)包含声明
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">main(argc,argv,envp)
其中 argc 是命令行中参数数目的计数,包括程序名称。对于上面的例子,
argc
argv
name = value
可能你觉得 execve 过于复杂,这时候我要鼓励一下你,execve 可能是 POSIX 的全部系统调用中最复杂的一个了,其他都比较简单。作为一个简单的例子,我们再来看一下
exit
UNIX 中的进程将内存划分成三个部分:
text segment,文本区
data segment,数据区
stack segment

上图能说明三个部分的内存分配情况,夹在中间的是空闲区,也就是未分配的区域,堆栈在需要时自动的挤压空闲区域,不过数据段的扩展是显示地通过系统调用
brk
malloc
许多系统调用都与文件系统有关,要读写一个文件,必须先将其打开。这个系统调用通过绝对路径名或指向工作目录的相对路径名指定要打开文件的名称,而代码
O_RDONLY
O_WRONLY
O_RDWR
O_CREATE
最常用的调用还是
read
write
尽管多数程序频繁的读写文件,但是仍有一些应用程序需要能够随机访问一个文件的任意部分。与每个文件相关的是一个指向文件当前位置的指针。在顺序读写时,该指针通常指向要读出(写入)的下一个字节。
Iseek
Iseek 有三个参数,
position = iseek(fd,offset,whence)
UNIX 为每个文件保存了该文件的类型(普通文件、特殊文件、目录等)、大小,最后修改时间以及其他信息,程序可以通过
stat
s = stat(name,&buf)
下面我们探讨目录和整个文件系统的系统调用,上面探讨的是和某个文件有关的系统调用。
mkdir
rmdir
s = mkdir(nname,mode)
s = rmdir(name)
s = link(name1,name2)

图中有两个用户
ast
jim
<pre class="brush:php;toolbar:false;">link("/usr/jim/memo", "/usr/ast/note");jim 中的 memo 文件现在会进入到 ast 的目录中,在 note 名称下。此后,
/usr/jim/memo
/usr/ast/note
要理解 link 是如何工作的需要清楚 link 做了什么操作。UNIX 中的每个文件都有一个独一无二的版本,也称作
i - number,i-编号
i-nodes,i-节点
unlink
就像我们上面提到过的那样,
mount
s = mount(special,name,flag)
通过执行 mount 系统调用,USB 文件系统可以被添加到根文件系统中,

图 a 是安装前的系统文件,图 b 是安装后的系统文件。
如果用 C 语言来执行那就是
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">mount("/dev/sdb0","/mnt",0)这里,第一个参数是 USB 驱动器 0 的块特殊文件名称,第二个参数是被安装在树中的位置,第三个参数说明将要安装的文件系统是可读写的还是只读的。
当不再需要一个文件系统时,可以使用 umount 移除之。
其他系统调用除了进程、文件、目录系统调用,也存在其他系统调用的情况,下面我们来探讨一下。我们可以看到上面其他系统调用只有四种,首先来看第一个 chdir,chdir 调用更改当前工作目录,在调用
代码语言:javascript代码运行次数:0运行复制<pre class="brush:php;toolbar:false;">chdir("/usr/ast/test");后,打开 xyz 文件,会打开
/usr/ast/test/xyz
在 UNIX 系统中,每个文件都会有保护模式,这个模式会有一个
读-写-执行
chmod
<pre class="brush:php;toolbar:false;">chmod("file",0644);kill
POSIX 定义了若干时间处理的进程。例如,
time
上面我们提到的都是 UNIX 系统调用,现在我们来聊聊 Win 32 中的系统调用。Windows 和 UNIX 在各自的编程方式上有着根本的不同。UNIX 程序由执行某些操作或执行其他操作的代码组成,进行系统调用以执行某些服务。Windows 系统则不同,Windows 应用程序通常是由事件驱动的。主程序会等待一些事件发生,然后调用程序去处理。最简单的事件处理是键盘敲击和鼠标滑过,或者是鼠标点击,或者是插入 USB 驱动,然后操作系统调用处理器去处理事件,更新屏幕和更新程序内部状态。这是与 UNIX 不同的设计风格。
当然,Windows 也有系统调用。在 UNIX 中,系统调用(比如 read)和系统调用所使用的调用库(例如 read)几乎是一对一的关系。而在 Windows 中,情况则大不相同。首先,函数库的调用和实际的系统调用几乎是不对应的。微软定义了一系列过程,称为
Win32应用编程接口(Application Programming Interface)
Win32 API 调用的数量是非常巨大的,有数千个多。但这些调用并不都是在内核态的模式下运行时,有一些是在用户态的模型下运行。Win32 API 有大量的调用,用来管理视窗、几何图形、文本、字体、滚动条、对话框、菜单以及 GUI 的其他功能。为了使图形子系统在内核态下运行,需要系统调用,否则就只有函数库调用。
我们把关注点放在和 Win32 系统调用中来,我们可以简单看一下 Win32 API 中的系统调用和 UNIX 中有什么不同(并不是所有的系统调用)

上表中是 UNIX 调用大致对应的 Win32 API 系统调用,简述一下上表。
CreateProcess
WaitForSingleObject
ExitProcess
然后是6个文件操作,在功能上和 UNIX 的调用类似,然而在参数和细节上是不同的。和 UNIX 中一样,文件可以打开,读取,写入,关闭。
SetFilePointer
GetFileAttributesEx
Windows 中有目录,目录分别用
CreateDirectory
RemoveDirectory
SetCurrentDirectory
GetLocalTime
Win32 接口中没有文件的链接、文件系统的 mount、umount 和 stat ,当然, Win32 中也有大量 UNIX 中没有的系统调用,特别是对 GUI 的管理和调用。
操作系统结构下面我们会探讨操作系统的几种结构,主要包括单体结构、分层系统、微内核、客户-服务端系统、虚拟机和外核等。下面以此来探讨一下
单体系统到目前为止,在大多数系统中,整个系统在内核态以单一程序的方式运行。整个操作系统是以程序集合来编写的,链接在一块形成一个大的二进制可执行程序。使用此技术时,如果系统中的每个过程都提供了前者所需的一些有用的计算,则它可以自由调用任何其他过程。在单体系统中,调用任何一个所需要的程序都非常高效,但是上千个不受限制的彼此调用往往非常臃肿和笨拙,而且单体系统必然存在单体问题,那就是只要系统发生故障,那么任何系统和应用程序将不可用,这往往是灾难性的。
在单体系统中构造实际目标程序时,会首先编译所有单个过程(或包含这些过程的文件),然后使用系统链接器将它们全部绑定到一个可执行文件中
对于单体系统,往往有下面几种建议
需要有一个主程序,用来调用请求服务程序需要一套服务过程,用来执行系统调用需要一套服务程序,用来辅助服务过程调用在单体系统中,对于每个系统调用都会有一个服务程序来保障和运行。需要一组实用程序来弥补服务程序需要的功能,例如从用户程序中获取数据。可将各种过程划分为一个三层模型

除了在计算机初启动时所装载的核心操作系统外,许多操作系统还支持额外的扩展。比如 I/O 设备驱动和文件系统。这些部件可以按需装载。在 UNIX 中把它们叫做
共享库(shared library)
动态链接库(Dynamic Link Library,DLL)
.dll
C:\Windows\system32
分层系统使用层来分隔不同的功能单元。每一层只与该层的上层和下层通信。每一层都使用下面的层来执行其功能。层之间的通信通过预定义的固定接口通信。

分层系统是由
E.W.Dijkstar
把上面单体系统进一步通用化,就变为了一个层次式结构的操作系统,它的上层软件都是在下层软件的基础之上构建的。该系统分为六层,如下所示
层号
功能
5
操作员
4
用户程序
3
输入/输出管理
2
操作员-进程通信
1
存储器和磁鼓管理
0
处理器分配和多道程序编程
处理器在 0 层运行,当中断发生或定时器到期时,由该层完成进程切换;在第 0 层之上,系统由一些连续的进程组成,编写这些进程时不用再考虑在单处理器上多进程运行的细节。内存管理在第 1 层,它分配进程的主存空间。第 1 层软件保证一旦需要访问某一页面,该页面必定已经在内存中,并且在页面不需要的时候将其移出。
第 2 层处理进程与操作员控制台(即用户)之间的通信。第 3 层管理 I/O 设备和相关的信息流缓冲区。第 4 层是用户程序层,用户程序不用考虑进程、内存、控制台或 I/O 设备管理等细节。系统操作员在第 5 层。
微内核在分层方式中,设计者要确定在哪里划分
内核-用户
所以,为了实现高可靠性,将操作系统划分成小的、层级之间能够更好定义的模块是很有必要的,只有一个模块 --- 微内核 --- 运行在内核态,其余模块可以作为普通用户进程运行。由于把每个设备驱动和文件系统分别作为普通用户进程,这些模块中的错误虽然会使这些模块崩溃,但是不会使整个系统死机。
MINIX 3

在内核的外部,系统的构造有三层,它们都在用户态下运行,最底层是设备驱动器。由于它们都在用户态下运行,所以不能物理的访问 I/O 端口空间,也不能直接发出 I/O 命令。相反,为了能够对 I/O 设备编程,驱动器构建一个结构,指明哪个参数值写到哪个 I/O 端口,并声称一个内核调用,这样就完成了一次调用过程。
位于用户态的驱动程序上面是
服务器
再生服务器(reincarnation server)
微内核中的内核还具有一种
机制
策略
微内核思想的策略是把进程划分为两类:
服务器
客户端
客户-服务器
客户-服务器模式会有两种载体,一种情况是一台计算机既是客户又是服务器,在这种方式下,操作系统会有某种优化;但是普遍情况下是客户端和服务器在不同的机器上,它们通过局域网或广域网连接。

客户通过发送消息与服务器通信,客户端并不需要知道这些消息是在本地机器上处理,还是通过网络被送到远程机器上处理。对于客户端而言,这两种情形是一样的:都是发送请求并得到回应。
越来越多的系统,包括家里的 PC,都成为客户端,而在某地运行的大型机器则成为服务器。许多 web 就是以这种方式运行的。一台 PC 向某个服务器请求一个 Web 页面,服务器把 Web 页面返回给客户端,这就是典型的客服-服务器模式
以上就是这些操作系统的概念,保你都听过!的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号