操作系统面试题(史上最全持续更新)

Posted 40岁资深老架构师尼恩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统面试题(史上最全持续更新)相关的知识,希望对你有一定的参考价值。

尼恩面试宝典专题40:操作系统面试题(史上最全、持续更新)

本文版本说明:V28

《尼恩面试宝典》升级规划为:

后续基本上,每一个月,都会发布一次,最新版本,可以联系构师尼恩获取, 发送 “领取电子书” 获取。


操作系统基础篇

在信息化时代,软件被称为计算机系统的灵魂。而作为软件核心的操作系统,已经与现代计算机系统密不可分、融为一体。计算机系统自下而上可粗分为四个部分:硬件操作系统应用程序用户。操作系统管理各种计算机硬件,为应用程序提供基础,并充当计算机硬件和用户的中介。

硬件,如中央处理器、内存、输入输出设备等,提供了基本的计算资源。应用程序,如字处理程序、电子制表软件、编译器、网络浏览器等,规定了按何种方式使用这些资源来解决用户的计算问题。操作系统控制和协调各用户的应用程序对硬件的使用。

综上所述,操作系统是指控制和管理整个计算机系统的硬件和软件资源,并合理的组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境集合。计算机操作系统是随着计算机研究和应用的发展逐步形成并发展起来的,它是计算机系统中最基本的系统软件。

操作系统的特征

操作系统是一种系统软件,但与其他的系统软件和应用软件有很大的不同,他有自己的特殊性即基本特征,操作系统的基本特征包括并发共享虚拟异步。这些概念对理解和掌握操作系统的核心至关重要。

并发

并发是指两个或多个事件在同一时间间隔内发生,在多道程序环境下,一段时间内宏观上有多个程序在同时执行,而在同一时刻,单处理器环境下实际上只有一个程序在执行,故微观上这些程序还是在分时的交替进行。操作系统的并发是通过分时得以实现的。操作系统的并发性是指计算机系统中同时存在多个运行着的程序,因此它具有处理和调度多个程序同时执行的能力。在操作系统中,引入进程的目的实施程序能并发执行。

共享

资源共享即共享,是指系统中的资源可供内存中多个并发执行的进程共同使用。共享可以分为以下两种资源共享方式。

互斥共享方式

系统中的某些资源,,如打印机、磁带机,虽然他们可以提供给多个进程使用,但为使所打印的内容不致造成混淆,应规定在同一段时间内只允许一个进程方位该资源。

为此,当进程a访问某资源时,必须先提出请求,如果此时该资源空闲,系统便可将之分配给进程a使用,伺候若再有其他进程也要访问该资源(只要a未用完)则必须等待。仅当进程a访问完并释放该资源后,才允许另一进城对该资源进行访问。计算机系统中的大所属物理设备,以及某些软件中所用的栈、变量和表格,都属于临界资源,他们都要求被互斥的共享。

同时访问方式

系统中还有一种资源,允许在一段时间内由多个进程“同时”对它进行访问。这里所谓的“同时”往往是宏观上的,而在微观上,这些进程可能是交替的对该资源进行访问即“分时共享”。典型的可供多个进程同时访问的资源是磁盘设备,一些用重入码编写的文件也可以被 “同时” 共享,即若干个用户同时访问该文件。

并发和共享是操作系统两个最基本的特征,这两者之间又是互为存在条件的:1资源共享是以程序的并发为条件的,若系统不允许程序并发执行,则自然不存在资源共享的问题;2若系统不能对资源共享实施有效地管理,也必将影响到程序的并发执行,甚至根本无法并发执行。

虚拟

虚拟是指把一个物理上的实体变为若干个逻辑上的对应物。物理实体是实的,即实际存在的;而后者是虚的,是用户感觉上的事物。相应的,用于实现虚拟的技术,成为虚拟技术。在操作系统中利用了多种虚拟技术,分别用来实现虚拟处理器、虚拟内存和虚拟外部设备。

在虚拟处理器技术中,是通过多道程序设计技术,让多道程序并发执行的方法,来分时使用一台处理器的。此时,虽然只有一台处理器,但他能同时为多个用户服务,是每个终端用户都认为是有一个中央处理器在为他服务。利用多道程序设计技术,把一台物理上的 CPU 虚拟为多台逻辑上的 CPU,称为虚拟处理器。

类似的,可以通过虚拟存储器技术,将一台机器的物理存储器变为虚拟存储器,一边从逻辑上来扩充存储器的容量。当然, 这是用户所感觉到的内存容量是虚的,我们把用户所发哦绝倒的存储器程序虚拟存储器。

还可以通过虚拟设备技术,将一台物理IO设备虚拟为多台逻辑上的IO设备,并允许每个用户占用一台逻辑上的 IO 设备,这样便可使原来仅允许在一段时间内有一个用户访问的设备,变为在一段时间内允许多个用户同时访问的共享设备。

因此操作系统的虚拟技术可归纳为:时分复用技术空分复用技术

异步

在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底,而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。

异步性使得操作系统运行在一种随机的环境下,可能导致进程产生于时间有关的错误。但是只要运行环境相同,操作系统必须保证多次运行进程,都获得相同的结果。

操作系统五大功能

一般来说,操作系统可以分为五大管理功能部分:

  1. 设备管理:主要是负责内核与外围设备的数据交互,实质是对硬件设备的管理,包括对输入输出设备的分配,初始化,维护与回收等。例如管理音频输入输出。
  2. 作业管理:这部分功能主要是负责人机交互,图形界面或者系统任务的管理。
  3. 文件管理:这部分功能涉及文件的逻辑组织和物理组织,目录结构和管理等。从操作系统的角度来看,文件系统是系统对文件存储器的存储空间进行分配,维护和回收,同时负责文件的索引,共享和权限保护。而从用户的角度来说,文件系统是按照文件目录和文件名来进行存取的。
  4. 进程管理:说明一个进程存在的唯一标志是 pcb(进程控制块),负责维护进程的信息和状态。进程管理实质上是系统采取某些进程调度算法来使处理合理的分配给每个任务使用。
  5. 存储管理:数据的存储方式和组织结构。

操作系统分类

操作系统的类型也可以分为几种:批处理系统,分时操作系统,实时操作系统,网络操作系统等。下面将简单的介绍他们各自的特点:

  1. 批处理系统:首先,用户提交完作业后并在获得结果之前不会再与操作系统进行数据交互,用户提交的作业由系统外存储存为后备作业;数据是成批处理的,有操作系统负责作业的自动完成;支持多道程序运行。
  2. 分时操作系统:首先交互性方面,用户可以对程序动态运行时对其加以控制;支持多个用户登录终端,并且每个用户共享CPU和其他系统资源。
  3. 实时操作系统:会有时钟管理,包括定时处理和延迟处理。实时性要求比较高,某些任务必须优先处理,而有些任务则会被延迟调度完成。
  4. 网络操作系统:网络操作系统主要有几种基本功能
    1. 网络通信:负责在源主机与目标主机之间的数据的可靠通信,这是最基本的功能。
    2. 网络服务:系统支持一些电子邮件服务,文件传输,数据共享,设备共享等。
    3. 资源管理:对网络中共享的资源进行管理,例如设置权限以保证数据源的安全性。
    4. 网络管理:主要任务是实现安全管理,例如通过“存取控制”来确保数据的存取安全性,通过“容错性”来保障服务器故障时数据的安全性。
    5. 支持交互操作:在客户/服务器模型的LAN环境下,多种客户机和主机不仅能与服务器进行数据连接通信,并且可以访问服务器的文件系统。

聊聊:什么是操作系统

操作系统是管理硬件和软件的一种应用程序。操作系统是运行在计算机上最重要的一种软件,它管理计算机的资源和进程以及所有的硬件和软件。它为计算机硬件和软件提供了一种中间层,使应用软件和硬件进行分离,让我们无需关注硬件的实现,把关注点更多放在软件应用上。

通常情况下,计算机上会运行着许多应用程序,它们都需要对内存和 CPU 进行交互,操作系统的目的就是为了保证这些访问和交互能够准确无误的进行。

聊聊:操作系统的主要功能

一般来说,现代操作系统主要提供下面几种功能

  • 进程管理: 进程管理的主要作用就是任务调度,在单核处理器下,操作系统会为每个进程分配一个任务,进程管理的工作十分简单;而在多核处理器下,操作系统除了要为进程分配任务外,还要解决处理器的调度、分配和回收等问题
  • 内存管理:内存管理主要是操作系统负责管理内存的分配、回收,在进程需要时分配内存以及在进程完成时回收内存,协调内存资源,通过合理的页面置换算法进行页面的换入换出
  • 设备管理:根据确定的设备分配原则对设备进行分配,使设备与主机能够并行工作,为用户提供良好的设备使用界面。
  • 文件管理:有效地管理文件的存储空间,合理地组织和管理文件系统,为文件访问和文件保护提供更有效的方法及手段。
  • 提供用户接口:操作系统提供了访问应用程序和硬件的接口,使用户能够通过应用程序发起系统调用从而操纵硬件,实现想要的功能。

聊聊:软件访问硬件的几种方式

软件访问硬件其实就是一种 IO 操作,软件访问硬件的方式,也就是 I/O 操作的方式有哪些。

硬件在 I/O 上大致分为并行和串行,同时也对应串行接口和并行接口。

随着计算机技术的发展,I/O 控制方式也在不断发展。选择和衡量 I/O 控制方式有如下三条原则

(1) 数据传送速度足够快,能满足用户的需求但又不丢失数据;

(2) 系统开销小,所需的处理控制程序少;

(3) 能充分发挥硬件资源的能力,使 I/O 设备尽可能忙,而 CPU 等待时间尽可能少。

根据以上控制原则,I/O 操作可以分为四类

  • 直接访问:直接访问由用户进程直接控制主存或 CPU 和外围设备之间的信息传送。直接程序控制方式又称为忙/等待方式。
  • 中断驱动:为了减少程序直接控制方式下 CPU 的等待时间以及提高系统的并行程度,系统引入了中断机制。中断机制引入后,外围设备仅当操作正常结束或异常结束时才向 CPU 发出中断请求。在 I/O 设备输入每个数据的过程中,由于无需 CPU 的干预,一定程度上实现了 CPU 与 I/O 设备的并行工作。

上述两种方法的特点都是以 CPU 为中心,数据传送通过一段程序来实现,软件的传送手段限制了数据传送的速度。接下来介绍的这两种 I/O 控制方式采用硬件的方法来显示 I/O 的控制

  • DMA 直接内存访问:为了进一步减少 CPU 对 I/O 操作的干预,防止因并行操作设备过多使 CPU 来不及处理或因速度不匹配而造成的数据丢失现象,引入了 DMA 控制方式。
  • 通道控制方式:通道,独立于 CPU 的专门负责输入输出控制的处理机,它控制设备与内存直接进行数据交换。有自己的通道指令,这些指令由 CPU 启动,并在操作结束时向 CPU 发出中断信号。

聊聊:操作系统的主要目的是什么

操作系统是一种软件,它的主要目的有三种

  • 管理计算机资源,这些资源包括 CPU、内存、磁盘驱动器、打印机等。
  • 提供一种图形界面,就像我们前面描述的那样,它提供了用户和计算机之间的桥梁。
  • 为其他软件提供服务,操作系统与软件进行交互,以便为其分配运行所需的任何必要资源。

聊聊:操作系统的种类有哪些

操作系统通常预装在你购买计算机之前。大部分用户都会使用默认的操作系统,但是你也可以升级甚至更改操作系统。但是一般常见的操作系统只有三种:Windows、macOS 和 Linux

聊聊:为什么 Linux 系统下的应用程序不能直接在 Windows 下运行

这是一个老生常谈的问题了,在这里给出具体的回答。

其中一点是因为 Linux 系统和 Windows 系统的格式不同,格式就是协议,就是在固定位置有意义的数据。Linux 下的可执行程序文件格式是 elf,可以使用 readelf 命令查看 elf 文件头。

而 Windows 下的可执行程序是 PE 格式,它是一种可移植的可执行文件。

还有一点是因为 Linux 系统和 Windows 系统的 API 不同,这个 API 指的就是操作系统的 API,Linux 中的 API 被称为系统调用,是通过 int 0x80 这个软中断实现的。而 Windows 中的 API 是放在动态链接库文件中的,也就是 Windows 开发人员所说的 DLL ,这是一个库,里面包含代码和数据。Linux 中的可执行程序获得系统资源的方法和 Windows 不一样,所以显然是不能在 Windows 中运行的。

聊聊:操作系统结构

单体系统

在大多数系统中,整个系统在内核态以单一程序的方式运行。整个操作系统是以程序集合来编写的,链接在一块形成一个大的二进制可执行程序,这种系统称为单体系统。

在单体系统中构造实际目标程序时,会首先编译所有单个过程(或包含这些过程的文件),然后使用系统链接器将它们全部绑定到一个可执行文件中

在单体系统中,对于每个系统调用都会有一个服务程序来保障和运行。需要一组实用程序来弥补服务程序需要的功能,例如从用户程序中获取数据。可将各种过程划分为一个三层模型

除了在计算机初启动时所装载的核心操作系统外,许多操作系统还支持额外的扩展。比如 I/O 设备驱动和文件系统。这些部件可以按需装载。在 UNIX 中把它们叫做 共享库(shared library),在 Windows 中则被称为 动态链接库(Dynamic Link Library,DLL)。他们的扩展名为 .dll,在 C:\\Windows\\system32 目录下存在 1000 多个 DLL 文件,所以不要轻易删除 C 盘文件,否则可能就炸了哦。

分层系统

分层系统使用层来分隔不同的功能单元。每一层只与该层的上层和下层通信。每一层都使用下面的层来执行其功能。层之间的通信通过预定义的固定接口通信。

微内核

为了实现高可靠性,将操作系统划分成小的、层级之间能够更好定义的模块是很有必要的,只有一个模块 — 微内核 — 运行在内核态,其余模块可以作为普通用户进程运行。由于把每个设备驱动和文件系统分别作为普通用户进程,这些模块中的错误虽然会使这些模块崩溃,但是不会使整个系统死机。

MINIX 3 是微内核的代表作,它的具体结构如下

在内核的外部,系统的构造有三层,它们都在用户态下运行,最底层是设备驱动器。由于它们都在用户态下运行,所以不能物理的访问 I/O 端口空间,也不能直接发出 I/O 命令。相反,为了能够对 I/O 设备编程,驱动器构建一个结构,指明哪个参数值写到哪个 I/O 端口,并声称一个内核调用,这样就完成了一次调用过程。

客户-服务器模式

微内核思想的策略是把进程划分为两类:服务器,每个服务器用来提供服务;客户端,使用这些服务。这个模式就是所谓的 客户-服务器模式。

客户-服务器模式会有两种载体,一种情况是一台计算机既是客户又是服务器,在这种方式下,操作系统会有某种优化;但是普遍情况下是客户端和服务器在不同的机器上,它们通过局域网或广域网连接。

客户通过发送消息与服务器通信,客户端并不需要知道这些消息是在本地机器上处理,还是通过网络被送到远程机器上处理。对于客户端而言,这两种情形是一样的:都是发送请求并得到回应。

聊聊:为什么称为陷入内核

如果把软件结构进行分层说明的话,应该是这个样子的,最外层是应用程序,里面是操作系统内核。

应用程序处于特权级 3,操作系统内核处于特权级 0 。如果用户程序想要访问操作系统资源时,会发起系统调用,陷入内核,这样 CPU 就进入了内核态,执行内核代码。至于为什么是陷入,我们看图,内核是一个凹陷的构造,有陷下去的感觉,所以称为陷入。

聊聊:什么是用户态和内核态

用户态和内核态是操作系统的两种运行状态。

  • 内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
  • 用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

那么为什么要有用户态和内核态呢?

这个主要是访问能力的限制的考量,计算机中有一些比较危险的操作,比如设置时钟、内存清理,这些都需要在内核态下完成,如果随意进行这些操作,那你的系统得崩溃多少次。

聊聊:用户态和内核态是如何切换的?

所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即用户态 -> 内核态 -> 用户态,而唯一能够做这些操作的只有 系统调用,而能够执行系统调用的就只有 操作系统

一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 陷阱指令(trap instruction)

他们的工作流程如下:

  • 首先用户程序会调用 glibc 库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。
  • glibc 库知道针对不同体系结构调用系统调用的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。
  • 然后,glibc 库调用软件中断指令(SWI) ,这个指令通过更新 CPSR 寄存器将模式改为超级用户模式,然后跳转到地址 0x08 处。
  • 到目前为止,整个过程仍处于用户态下,在执行 SWI 指令后,允许进程执行内核代码,MMU 现在允许内核虚拟内存访问
  • 从地址 0x08 开始,进程执行加载并跳转到中断处理程序,这个程序就是 ARM 中的 vector_swi()
  • 在 vector_swi() 处,从 SWI 指令中提取系统调用号 SCNO,然后使用 SCNO 作为系统调用表 sys_call_table 的索引,调转到系统调用函数。
  • 执行系统调用完成后,将还原用户模式寄存器,然后再以用户模式执行。

聊聊:什么是内核

在计算机中,内核是一个计算机程序,它是操作系统的核心,可以控制操作系统中所有的内容。内核通常是在 boot loader 装载程序之前加载的第一个程序。

这里还需要了解一下什么是 boot loader

boot loader 又被称为引导加载程序,能够将计算机的操作系统放入内存中。在电源通电或者计算机重启时,Bios 会执行一些初始测试,然后将控制权转移到引导加载程序所在的主引导记录(MBR)

聊聊:什么是实时系统

实时操作系统对时间做出了严格的要求,实时操作系统分为两种:硬实时和软实时

硬实时操作系统规定某个动作必须在规定的时刻内完成或发生,比如汽车生产车间,焊接机器必须在某一时刻内完成焊接,焊接的太早或者太晚都会对汽车造成永久性伤害。

软实时操作系统虽然不希望偶尔违反最终的时限要求,但是仍然可以接受。并且不会引起任何永久性伤害。比如数字音频、多媒体、手机都是属于软实时操作系统。

你可以简单理解硬实时和软实时的两个指标:是否在时刻内必须完成以及是否造成严重损害

聊聊:Linux 操作系统的启动过程

当计算机电源通电后,BIOS会进行开机自检(Power-On-Self-Test, POST),对硬件进行检测和初始化。因为操作系统的启动会使用到磁盘、屏幕、键盘、鼠标等设备。

下一步,磁盘中的第一个分区,也被称为 MBR(Master Boot Record) 主引导记录,被读入到一个固定的内存区域并执行。这个分区中有一个非常小的,只有 512 字节的程序。程序从磁盘中调入 boot 独立程序,boot 程序将自身复制到高位地址的内存从而为操作系统释放低位地址的内存。

复制完成后,boot 程序读取启动设备的根目录。boot 程序要理解文件系统和目录格式。然后 boot 程序被调入内核,把控制权移交给内核。直到这里,boot 完成了它的工作。系统内核开始运行。

内核启动代码是使用汇编语言完成的,主要包括创建内核堆栈、识别 CPU 类型、计算内存、禁用中断、启动内存管理单元等,然后调用 C 语言的 main 函数执行操作系统部分。

这部分也会做很多事情,首先会分配一个消息缓冲区来存放调试出现的问题,调试信息会写入缓冲区。如果调试出现错误,这些信息可以通过诊断程序调出来。

然后操作系统会进行自动配置,检测设备,加载配置文件,被检测设备如果做出响应,就会被添加到已链接的设备表中,如果没有相应,就归为未连接直接忽略。

配置完所有硬件后,接下来要做的就是仔细手工处理进程0,设置其堆栈,然后运行它,执行初始化、配置时钟、挂载文件系统。创建 init 进程(进程 1 )守护进程(进程 2)

init 进程会检测它的标志以确定它是否为单用户还是多用户服务。在前一种情况中,它会调用 fork 函数创建一个 shell 进程,并且等待这个进程结束。后一种情况调用 fork 函数创建一个运行系统初始化的 shell 脚本(即 /etc/rc)的进程,这个进程可以进行文件系统一致性检测、挂载文件系统、开启守护进程等。

然后 /etc/rc 这个进程会从 /etc/ttys 中读取数据,/etc/ttys 列出了所有的终端和属性。对于每一个启用的终端,这个进程调用 fork 函数创建一个自身的副本,进行内部处理并运行一个名为 getty 的程序。

getty 程序会在终端上输入

login:

等待用户输入用户名,在输入用户名后,getty 程序结束,登陆程序 /bin/login 开始运行。login 程序需要输入密码,并与保存在 /etc/passwd 中的密码进行对比,如果输入正确,login 程序以用户 shell 程序替换自身,等待第一个命令。如果不正确,login 程序要求输入另一个用户名。

整个系统启动过程如下

进程线程协程篇

系统调度

在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行;在多道程序环境下,则允许多个程序并发执行。程序的这两种执行方式间有着显著的不同。也正是程序并发执行时的这种特征,才导致了在操作系统中引入进程的概念。进程是资源分配的基本单位,线程是资源调度的基本单位。

应用启动体现的就是静态指令加载进内存,进而进入 CPU 运算,操作系统在内存开辟了一段栈内存用来存放指令和变量值,从而形成了进程。早期的操作系统基于进程来调度 CPU,不同进程间是不共享内存空间的,所以进程要做任务切换就要切换内存映射地址。由于进程的上下文关联的变量,引用,计数器等现场数据占用了打段的内存空间,所以频繁切换进程需要整理一大段内存空间来保存未执行完的进程现场,等下次轮到 CPU 时间片再恢复现场进行运算。

这样既耗费时间又浪费空间,所以我们才要研究多线程。一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了。现代的操作系统都基于更轻量的线程来调度,现在我们提到的 “任务切换” 都是指 “线程切换”。

进程详解

进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。所谓的并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。无论是在单核还是多核系统中,可以通过处理器在进程间切换,来实现单个 CPU 看上去像是在并发地执行多个进程。操作系统实现这种交错执行的机制称为上下文切换。

操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,它包括许多信息,例如 PC 和寄存器文件的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从上次停止的地方开始。

操作系统为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的是一致的存储器,称为虚拟地址空间。其虚拟地址空间最上面的区域是为操作系统中的代码和数据保留的,这对所有进程来说都是一样的;地址空间的底部区域存放用户进程定义的代码和数据。

程序代码和数据:对于所有的进程来说,代码是从同一固定地址开始,直接按照可执行目标文件的内容初始化。

:代码和数据区后紧随着的是运行时堆。代码和数据区是在进程一开始运行时就被规定了大小,与此不同,当调用如 malloc 和 free 这样的 C 语言 标准库函数时,堆可以在运行时动态地扩展和收缩。

共享库:大约在地址空间的中间部分是一块用来存放像 C 标准库和数学库这样共享库的代码和数据的区域。

:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

内核虚拟存储器:内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。

线程详解

在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。

而任何一个子线程终止一般不会影响其他线程,除非子线程执行了 exit() 系统调用。任何一个子线程执行 exit(),全部线程同时灭亡。多线程程序中至少有一个主线程,而这个主线程其实就是有 main 函数的进程。它是整个程序的进程,所有线程都是它的子线程;我们通常把具有多线程的主进程称之为主线程。

线程共享的环境包括:进程代码段、进程的公有数据、进程打开的文件描述符、信号的处理器、进程的当前目录、进程用户 ID 与进程组 ID 等,利用这些共享的数据,线程很容易的实现相互之间的通讯。线程拥有这许多共性的同时,还拥有自己的个性,并以此实现并发性:

线程 ID:每个线程都有自己的线程 ID,这个 ID 在本进程中是唯一的。进程用此来标识线程。

寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便 将来该线程在被重新切换到时能得以恢复。

线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。

错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了 errno 值,而在该 线程还没有处理这个错误,另外一个线程就在此时 被调度器投入运行,这样错误值就有可能被修改。 所以,不同的线程应该拥有自己的错误返回码变量。

线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

线程模型

线程实现在用户空间下

当线程在用户空间下实现时,操作系统对线程的存在一无所知,操作系统只能看到进程,而不能看到线程。所有的线程都是在用户空间实现。在操作系统看来,每一个进程只有一个线程。过去的操作系统大部分是这种实现方式,这种方式的好处之一就是即使操作系统不支持线程,也可以通过库函数来支持线程。

在这在模型下,程序员需要自己实现线程的数据结构、创建销毁和调度维护。也就相当于需要实现一个自己的线程调度内核,而同时这些线程运行在操作系统的一个进程内,最后操作系统直接对进程进行调度。

这样做有一些优点,首先就是确实在操作系统中实现了真实的多线程,其次就是线程的调度只是在用户态,减少了操作系统从内核态到用户态的切换开销。这种模式最致命的缺点也是由于操作系统不知道线程的存在,因此当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作。还有一个问题是假如进程中一个线程长时间不释放 CPU,因为用户空间并没有时钟中断机制,会导致此进程中的其它线程得不到 CPU 而持续等待。

线程实现在操作系统内核中

内核线程就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。

程序员直接使用操作系统中已经实现的线程,而线程的创建、销毁、调度和维护,都是靠操作系统(准确的说是内核)来实现,程序员只需要使用系统调用,而不需要自己设计线程的调度算法和线程对 CPU 资源的抢占使用。

使用用户线程加轻量级进程混合实现

在这种混合实现下,即存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系:

Golang 的协程就是使用了这种模型,在用户态,协程能快速的切换,避免了线程调度的 CPU 开销问题,协程相当于线程的线程。

Linux中的线程

在 Linux 2.4 版以前,线程的实现和管理方式就是完全按照进程方式实现的;在 Linux 2.6 之前,内核并不支持线程的概念,仅通过轻量级进程(Lightweight Process)模拟线程;轻量级进程是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。这种模型最大的特点是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库(Linux Thread)函数完成的。

为了完全兼容 Posix 标准,Linux 2.6 首先对内核进行了改进,引入了线程组的概念(仍然用轻量级进程表示线程),有了这个概念就可以将一组线程组织称为一个进程,不过内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程;相反,线程仅仅被视为一个与其他进程(概念上应该是线程)共享某些资源的进程(概念上应该是线程)。在实现上主要的改变就是在 task_struct 中加入 tgid 字段,这个字段就是用于表示线程组 id 的字段。在用户线程库方面,也使用 NPTL 代替 Linux Thread,不同调度模型上仍然采用 1 对 1 模型。

进程的实现是调用 fork 系统调用:pid_t fork(void);,线程的实现是调用 clone 系统调用:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...)。与标准 fork() 相比,线程带来的开销非常小,内核无需单独复制进程的内存空间或文件描写叙述符等等。这就节省了大量的 CPU 时间,使得线程创建比新进程创建快上十到一百倍,能够大量使用线程而无需太过于操心带来的 CPU 或内存不足。无论是 fork、vfork、kthread_create 最后都是要调用 do_fork,而 do_fork 就是根据不同的函数参数,对一个进程所需的资源进行分配。

内核线程

内核线程是由内核自己创建的线程,也叫做守护线程(Deamon),在终端上用命令 ps -Al 列出的所有进程中,名字以 k 开关以 d 结尾的往往都是内核线程,比如 kthreadd、kswapd 等。与用户线程相比,它们都由 do_fork() 创建,每个线程都有独立的 task_struct 和内核栈;也都参与调度,内核线程也有优先级,会被调度器平等地换入换出。二者的不同之处在于,内核线程只工作在内核态中;而用户线程则既可以运行在内核态(执行系统调用时),也可以运行在用户态;内核线程没有用户空间,所以对于一个内核线程来说,它的 0~3G 的内存空间是空白的,它的 current->mm 是空的,与内核使用同一张页表;而用户线程则可以看到完整的 0~4G 内存空间。

在 Linux 内核启动的最后阶段,系统会创建两个内核线程,一个是 init,一个是 kthreadd。其中 init 线程的作用是运行文件系统上的一系列”init”脚本,并启动 shell 进程,所以 init 线程称得上是系统中所有用户进程的祖先,它的 pid 是 1。kthreadd 线程是内核的守护线程,在内核正常工作时,它永远不退出,是一个死循环,它的 pid 是 2。

协程

协程是用户模式下的轻量级线程,最准确的名字应该叫用户空间线程(User Space Thread),在不同的领域中也有不同的叫法,譬如纤程(Fiber)、绿色线程(Green Thread)等等。操作系统内核对协程一无所知,协程的调度完全有应用程序来控制,操作系统不管这部分的调度;一个线程可以包含一个或多个协程,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上细纹和栈保存起来,在切换回来时恢复先前保运的寄存上下文和栈。

协程的优势如下:

  • 节省内存,每个线程需要分配一段栈内存,以及内核里的一些资源
  • 节省分配线程的开销(创建和销毁线程要各做一次 syscall)
  • 节省大量线程切换带来的开销
  • 与 NIO 配合实现非阻塞的编程,提高系统的吞吐

比如 Golang 里的 go 关键字其实就是负责开启一个 Fiber,让 func 逻辑跑在上面。而这一切都是发生的用户态上,没有发生在内核态上,也就是说没有 ContextSwitch 上的开销。

Go协程模型

Go 线程模型属于多对多线程模型,在操作系统提供的内核线程之上,Go 搭建了一个特有的两级线程模型。Go 中使用使用 Go 语句创建的 Goroutine 可以认为是轻量级的用户线程,Go 线程模型包含三个概念:

G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。

P: Processor,表示逻辑处理器,对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。

M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。

在 Go 中每个逻辑处理器§会绑定到某一个内核线程上,每个逻辑处理器(P)内有一个本地队列,用来存放 Go 运行时分配的 goroutine。多对多线程模型中是操作系统调度线程在物理 CPU 上运行,在 Go 中则是 Go 的运行时调度 Goroutine 在逻辑处理器(P)上运行。

Go 的栈是动态分配大小的,随着存储数据的数量而增长和收缩。每个新建的 Goroutine 只有大约 4KB 的栈。每个栈只有 4KB,那么在一个 1GB 的 RAM 上,我们就可以有 256 万个 Goroutine 了,相对于 Java 中每个线程的 1MB,这是巨大的提升。Golang 实现了自己的调度器,允许众多的 Goroutines 运行在相同的 OS 线程上。就算 Go 会运行与内核相同的上下文切换,但是它能够避免切换至 ring-0 以运行内核,然后再切换回来,这样就会节省大量的时间。

在 Go 中存在两级调度:

  • 一级是操作系统的调度系统,该调度系统调度逻辑处理器占用 cpu 时间片运行;
  • 一级是 Go 的运行时调度系统,该调度系统调度某个 Goroutine 在逻辑处理上运行。

使用 Go 语句创建一个 Goroutine 后,创建的 Goroutine 会被放入 Go 运行时调度器的全局运行队列中,然后 Go 运行时调度器会把全局队列中的 Goroutine 分配给不同的逻辑处理器(P),分配的 Goroutine 会被放到逻辑处理器(P)的本地队列中,当本地队列中某个 Goroutine 就绪后待分配到时间片后就可以在逻辑处理器上运行了。

进程线程协程详解总结

进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。

在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。

协程是用户模式下的轻量级线程,最准确的名字应该叫用户空间线程(User Space Thread)。

进程线程协程区别

进程协程进程对比

进程概念

进程是系统资源分配的最小单位, 系统由一个个进程(程序)组成 一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。

文本区域存储处理器执行的代码,数据区域存储变量和进程执行期间使用的动态分配的内存,堆栈区域存储着活动过程调用的指令和本地变量。

因此进程的创建和销毁都是相对于系统资源,所以是一种比较昂贵的操作。 进程有三个状态:

状态描述
等待态等待某个事件的完成
就绪态等待系统分配处理器以便运行
运行态占有处理器正在运行

进程是抢占式的争夺 CPU 运行自身,而 CPU 单核的情况下同一时间只能执行一个进程的代码,但是多进程的实现则是通过 CPU 飞快的切换不同进程,因此使得看上去就像是多个进程在同时进行。

通信问题:由于进程间是隔离的,各自拥有自己的内存内存资源, 因此相对于线程比较安全, 所以不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享。

线程概念

线程属于进程,线程共享进程的内存地址空间并且线程几乎不占有系统资源。

通信问题: 进程相当于一个容器,而线程而是运行在容器里面的,因此对于容器内的东西,线程是共同享有的,因此线程间的通信可以直接通过全局变量进行通信。但是由此带来的例如多个线程读写同一个地址变量的时候则将带来不可预期的后果,因此这时候引入了各种锁的作用,例如互斥锁等。

同时多线程是不安全的,当一个线程崩溃了,会导致整个进程也崩溃了,即其他线程也挂了, 但多进程而不会,一个进程挂了,另一个进程依然照样运行。

进程是系统分配资源的最小单位,线程是 CPU 调度的最小单位。由于默认进程内只有一个线程,所以多核 CPU 处理多进程就像是一个进程一个核心。

协程概念

协程是属于线程的。协程程序是在线程里面跑的,因此协程又称微线程和纤程等,协没有线程的上下文切换消耗。协程的调度切换是用户(程序员)手动切换的,因此更加灵活,因此又叫用户空间线程。

原子操作性。由于协程是用户调度的,所以不会出现执行一半的代码片段被强制中断了,因此无需原子操作锁。

进程线程协程详解

进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

图解

线程图解如下:

协程图解如下:

进程与线程比较

  1. 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间。
  2. 资源拥有: 进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
  3. 线程是处理器调度的基本单位,但进程不是。
  4. 二者均可并发执行。
  5. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

协程与线程进行比较

  1. 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
  2. 线程进程都是同步机制,而协程则是异步。
  3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。

进程线程协程区别总结

进程是系统资源分配的最小单位, 系统由一个个进程(程序)组成 一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。

线程属于进程,线程共享进程的内存地址空间并且线程几乎不占有系统资源。

协程是属于线程的。协程程序是在线程里面跑的,因此协程又称微线程和纤程等,协没有线程的上下文切换消耗。协程的调度切换是用户(程序员)手动切换的,因此更加灵活,因此又叫用户空间线程。

孤儿进程

孤儿进程教程

如果父进程先退出,子进程还没退出那么子进程将被托孤给 init 进程,这时子进程的父进程就是 init 进程(1 号进程)。

案例

创建孤儿进程

我们在 Linux 下使用 vim 新建一个 childprocess.c 的文件,编写如下 C 语言 代码如下:

#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <signal.h>
int main(void)

        pid_t pid ;
        signal(SIGCHLD,SIG_IGN);
        printf("before fork pid:%d\\n",getpid());
        int abc = 10;
        pid = fork();
        if(pid == -1)
        
                perror("tile");
                return -1;
        
        if(pid > 0)           //父进程先退出
        
                abc++;
                printf("parent:pid:%d \\n",getpid());
                printf("abc:%d \\n",abc);
                sleep(5);
        
        else if(pid == 0)    //值进程后退出,被托付给init进程
          
                abc++;
                printf("child:%d,parent: %d\\n",getpid(),getppid());
                printf("abc:%d",abc);
                sleep(100);
        
        printf("fork after...\\n");

我们使用 gcc 编译上述程序,具体命令如下:

gcc childprocess.c -ochildprocess

编译完成后,会在当前目录生成一个 childprocess 的二进制可执行文件,我们使用 ls 命令,查看,如下:

此时,我们直接运行该二进制文件,输入以下命令:

./childprocess

运行成功后,控制台输出如下:

此时,我们在另一终端,使用 ps 命令,查看当前进程的状态,具体命令如下:

ps -elf | grep childprocess

此时,运

以上是关于操作系统面试题(史上最全持续更新)的主要内容,如果未能解决你的问题,请参考以下文章

Nginx面试题(史上最全 + 持续更新)

Nginx面试题(史上最全 + 持续更新)

Nginx面试题(史上最全 + 持续更新)

Docker面试题(史上最全 + 持续更新)

Docker面试题(史上最全 + 持续更新)

K8S面试题(史上最全 + 持续更新)