其他分享
首页 > 其他分享> > 操作系统面试题汇总

操作系统面试题汇总

作者:互联网

进程与线程

进程和线程的区别

进程 线程
定义 资源调度的基本单位 程序执行的基本单位
切换 指令 + 资源(栈、寄存器、页表、文件句柄等) 指令(线程栈、寄存器和 PC)
通信 进程通信需要借助操作系统 线程可以读写进程数据段来进行通信
开销 切换虚拟空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换 保存和设置少量寄存器内容

做一次简单的i = i + 1在计算机中并不是原子操作,涉及内存取数,计算和写入内存几个环节,而线程的切换有可能发生在上述任何一个环节中间,所以不同的操作顺序很有可能带来意想不到的结果。

将顺序执行的程序(暂时不考虑多进程)拆分成几个独立的逻辑流,这些逻辑流可以独立完成一些任务。

是否使用多进程、多线程,弄清以下两点非常重要:

同一进程的多个线程共享进程的资源。每个线程除了标识线程的tid,还有自己独立的栈空间,线程彼此之间是无法访问其他线程栈上内容的。

线程作为处理机调度的最小单位,线程调度只需要保存线程栈、寄存器数据和 PC 即可,相比进程切换开销要小很多。

多进程和多线程区别

进程对应的内存空间:

全局的未初始化变量存放在 .bss 中,具体体现为一个占位符(不分配空间只记录数据所需的空间的大小),.bss 不占用可执行文件空间,由 OS 初始化(清零)。

C++ 程序对应部分:

父、子进程

父进程创建子进程之后,父、子进程除了 pid 外,其余部分几乎一样,共享全部数据,但不是同一数据块。子进程在读写数据时会通过写时复制机制将公共数据重新拷贝一份,在拷贝的数据上进行操作。

子进程想要运行自己的代码段,还可以通过调用 execv() 函数重新加载新的代码段,之后就和父进程独立开了。

Linux 进程控制

虚拟存储器为每个进程提供了独立占系统地址空间的假象。

对于32位进程的地址空间都是 4GB,用户态只能访问低 3GB 的地址空间,3GB~4GB 是内核态地址空间。

进程的调度实际就是内核选择相应的进程控制块(PCB)。

内核管理所有的 PCB,每一次进程调度就是一次上下文切换,其本质就是当前运行状态的切换,包括通用寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈和内核数据结构(页表、进程表、文件表)等。

一次完整的 A、B 进程切换(A用户态 -> A内核态 -> B内核态 -> B用户态)

进程调度算法

进程间通信

不同进程间的通信本质:进程之间可以看到一份公共资源,而提供这份资源的形式或者提供者不同,造成了通信方式不同。

进程间的关系

一个进程可以创建多少个线程

进程同步机制

概念

遵循原则

同步机制——锁

只能用于线程同步

同步机制——信号量

可用于进程同步,也可用于线程同步

进程同步的四种方法

  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
    • 优点:保证同一时刻只有一个线程能访问数据的简单办法;
    • 缺点:虽然临界区同步速度块,但只能是本进程内的线程,不可用来同步多进程的线程。
  2. 互斥量(Mutex):为协调共同对一个共享资源的单独访问而设计。跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
    • 优点:能应用到不同进程的不同线程之间实现对资源的安全共享;
    • 缺点:1. 创建互斥量需要的资源更多 2. 互斥量一次只能一个用户来访问,对于多用户的查询不适用。
  3. 信号量:为控制一个具有有限数量资源而设计的,它允许多个线程在同一时刻访问同一资源。互斥量是信号量的特殊情况,信号量 = 1。
    • 优点:适用于对Socket(套接字)程序中线程的同步。(例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。)
    • 缺点:1. 信号量机制必须有公共内存,不能用于分布式操作系统; 2. 读写和维护复杂;3. 核心 P-V 分散在各用户程序中,不易控制和管理;
  4. 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始
    • 优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。

  • 临界区不是内核对象,只能用于进程内部的线程同步,是用户方式的同步。互斥、信号量是内核对象可以用于不同进程之间的线程同步(跨进程同步)。
  • 互斥其实是信号量的一种特殊形式。互斥可以保证在某一时刻只有一个线程可以拥有临界资源。信号量可以保证在某一时刻有指定数目的线程可以拥有临界资源。

进程状态切换

  • 就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
  • 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。

终端退出时,终端运行的进程会怎么样?

终端在退出时会发送 SIGHUP 给对应的 bash 进程,bash 进程收到这个信号后首先将它发给 session 下面的进程,如果程序没有对 SIGHUP 信号做特殊处理,那么进程就会随着终端关闭而退出。

如何让进程后台运行

  1. 命令后加 &,实际上,这是将命令放入到一个作业队列中了;
  2. ctrl + z 挂起进程,使用 jobs 查看序号,在使用 bg %序号 后台运行进程;
  3. nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断(SIGHUP)信号
  4. 运行指令前面 + setsid,使其父进程编程init进程,不受HUP信号的影响
  5. 将 命令+ &放在()括号中,也可以是进程不受HUP信号的影响

怎么创建守护进程

Linux 守护进程是运行在后台的一种特殊进程,它独立于控制终端,并周期性地执行某种任务或等待处理某些发生的事件。

守护进程的父进程是 init 进程,它真正的父进程在 fork 出子进程后就先于子进程退出勒,所以它是一个由 init 继承的孤儿进程。

守护进程名称通常以 d 结尾

创建守护进程要点:

  1. 让程序后台执行。方法是调用 fork() 产生一个子进程,然后使父进程退出;
  2. 调用 setsid() 创建一个新会话。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们影响,可以调用 setsid() 使进程成为一个会话组长。 setsid() 调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
  3. 禁止进程重新打开终端。以上步骤使进程成为一个无终端的会话组长,但它可以重新打开一个终端。
  4. 关闭不需要的文件描述符。子进程会从父进程继承打开的文件描述符,如不关闭,会造成资源浪费,进程所在的文件系统无法卸载而引发错误。
  5. 将当前目录更改为根目录。调用 chdir 函数,让根目录 ”/” 成为子进程的工作目录;
  6. 子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
  7. 处理 SIGCHLD 信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。

孤儿进程

如果父进程先退出,子进程还没有退出,那么子进程的父进程将变成 init 进程。(由 init 收养)

僵尸进程

子进程先退出,父进程没有退出,那么子进程必须等父进程捕获到子进程退出的状态才能真正退出,否则这时候子进程就成为勒僵尸进程。

设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。

如何避免僵尸进程

忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

为什么只能运行一个前台作业

shell分前后台来控制的不是进程而是作业(job)或者进程组(Process Group)。

一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,shell 可以运行一个前台作业和任意多个后台作业,这称为作业控制。

我们在前台新起了一个作业,shell 就被提到了后台,因此 shell 就没有办法再继续接受我们的指令并且解析运行了。 但是如果前台进程退出了,shell 就会有被提到前台来,就可以继续接受我们的命令并且解析运行。

作业与进程组的区别

如果作业中的某个进程有创建了子进程,则该子进程是不属于该作业的。 一旦作业运行结束,shell就把自己提到前台(子进程还存在,但是子进程不属于作业),如果原来的前台进程还存在(这个子进程还没有终止),他将自动变为后台进程组

会话

会话(Session)是一个或多个进程组的集合。一个会话可以有一个控制终端。在xshell或者WinSCP中打开一个窗口就是新建一个会话。

进程终止的几种方式

  1. main 函数自然返回,return
  2. 调用 exit 函数,C 库函数调用
  3. 调用 _exit 函数,属于系统调用
  4. 调用 abort 函数,程序异常终止,同时发送 GIGABRT 信号给调用进程
  5. 接受能导致进程终止的信号:ctrl+c,SIGINT

交换空间与虚拟内存的关系

存储管理

什么是快表

快表,又称联想寄存器(TLB) ,是一种高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。与此对应,内存中的页表常称为慢表。

快表下地址转换过程

  1. CPU 给出逻辑地址,由硬件算出页号、页内偏移量;
  2. 检查页号合法性;
  3. 将页号与快表中页号匹配;
  4. 若命中,则直接从快表取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后访问该物理地址对应的内存单元。因此,若快秒命中,则只需要一次访存;
  5. 若没有命中,则需要再访问内存中的页表,找到对应页表项,得到内存块号,再将内存块号与页内偏移量拼接形成物理地址。
  6. 最后访问该物理地址对应的内存单元。

若没有命中,需要两次访存,并将页表存入快表。(FIFO、LRU、OPT)。

内存交换和覆盖的区别

交换是不同进程(或作业)之间进行,覆盖用于同一进程。

动态分区分配算法

  1. 首次适应算法
    每次都从低地址开始查找,找到第一个能满足大小的空闲分区。(空闲分区链/空闲分区表)

  2. 邻近适应算法
    首次适应算法每次从链头开始,容易导致低地址部分出现很多小的空闲区,并且从头开始开销也大,邻近适应算法从上次结束的位置开始检索。(实现:循环链表)

  3. 最佳适应算法
    由于动态分区分配是一种连续分配方式,为了保证内存不浪费,为“大进程”留空间,优先选择适合当前需求的最小空闲区,但会造成许多碎片。(空闲分区按容量递增排列,空闲分区链/表)

  4. 最坏(最大)适应算法
    每次分配时优先使用最大的连续空闲区,这样剩下的空闲区不会太小,方便使用。(实现:空闲分区按容量递减排列,空闲分区链/表)

实际上:首次适应最好,邻近会造成末尾分配空间分裂成小碎片,比首次要差,最佳导致大量碎片,最坏导致没有大的空间。

虚拟技术

虚拟技术把一个物理实体转换为多个逻辑实体。

主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。

时分复用:多进程和多线程能在一个处理器上并发执行。并发(逻辑)、并行(物理)

虚拟内存使用空分复用技术,将物理内存抽象为地址空间,每个进程都有各自的地址空间,地址空间的页被隐射到物理内存,地址空间的页并不需要全部在物理内存中,当使用一个没有在物理内存的页时,执行页面置换算法,将页置换到内存中。

虚拟内存的目的

虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,在程序的角度这个地址空间是连续的一段,但实际上这个地址空间被分割成多个块,每一块称为一页。

这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。这样程序不需要全部调入内存就可以运行,使有限内存运行大程序称为可能。

逻辑地址转换为物理地址的基本过程

可以借助进程的页表将逻辑地址转换为物理地址。

进程未执行时,页表的起始地址和页表长度会在进程控制块(PCB)中,当进程被调度时,会放到页表寄存器(PTR)中。

页面大小是2的整数幂 设页面大小为L,逻辑地址A到物理地址E的变换过程如下:

  1. 根据 PCB 逻辑地址计算出页号、页内偏移量
  2. 判断页号是否越界
  3. 查找页表,找到页号对应的页表项,确定页面
  4. 用内存块号和页内偏移量得到物理地址
  5. 访问目标内存单元

操作系统内存管理的工作

  1. 负责内存空间的分配与回收
  2. 从逻辑上对内存空间进行扩充
  3. 负责程序的逻辑地址与物理地址的转换
  4. 提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰。

什么是内存?作用是什么?

执行 malloc 申请内存时,操作系统是怎么做的

从操作系统层面上看,malloc是通过两个系统调用来实现的: brk 和 mmap

通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。

进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。

抖动、颠簸现象

刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)

为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率 为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程工作集” 的概念

堆、栈分配效率比较

从两个方面考虑:

常见的内存分配方式

  1. 从静态存储区分配。内存在编译时就分配好,在整个程序运行期间都存在,例如全局变量、static 变量;
  2. 在栈上创建。栈内存分配效率很高,但分配的内存容量有限;
  3. 从堆上创建,也叫动态内存分内,需要手动 malloc/new,自己负责 free/delete。

常见内存分配错误

  1. 分配未成功就使用。例如指针为空。
  2. 分配成功,但未初始化。如数组。
  3. 操作越过内存边界。
  4. 忘记释放内存,造成内存泄漏。
  5. 释放了内存却继续使用
    • 程序调用过于复杂,不知道某对象是否已释放;
    • return 语句写错,不要返回指向"栈内存"的指针或引用;
    • free/delete 后,没有将指针设为 NULL,导致野指针。

发生内存交换时,进程的考虑

ASCII、Unicode和UTF-8编码的区别

三者之间联系:

  1. 在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF-8 编码
  2. 用记事本编辑的时候,从文件读取的 UTF-8 字符被转换为 Unicode 字符到内存里,编辑完成后,保存的时候再把 Unicode 转换为 UTF-8 保存到文件。
  3. 浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:

内存交换需要注意的关键点

  1. 交换需要备份存储,通常是快速磁盘,它必须足够大,并且提供对这些内存映像的直接访问。
  2. 为了有效使用CPU,需要每个进程的执行时间比交换时间长,而影响交换时间的主要是转移时间,转移时间与所交换的空间内存成正比。
  3. 如果换出进程,比如确保该进程的内存空间成正比。
  4. 交换空间通常作为磁盘的一整块,且独立于文件系统,因此使用就可能很快。
  5. 交换通常在有许多进程运行且内存空间吃紧时开始启动,而系统负荷降低就暂停。
  6. 普通交换使用不多,但交换的策略的某些变种在许多系统中(如UNIX系统)仍然发挥作用。

分段式分配和固定分区分配

分段式存储是按需分配,有外部碎片而无内部碎片
固定式分配是固定分配的方式

内部碎片和外部碎片

内部碎片:分配给某些进程的内存区域中有些部分没用上,常见于固定分配方式
外部碎片:内存中某些空闲区因为比较小,难以利用上,一般出现在内存动态分配方式中

如何消除碎片文件

外部碎片:通过紧凑技术消除,即操作系统不时对进程进行移动和整理,需要动态重定位寄存器的支持,且费时
通过内存交换。

死锁

几种典型的锁?

死锁产生原因

理论上认为死锁产生有以下四个必要条件,缺一不可:

  1. 互斥条件:进程对所需求的资源具有排他性;
  2. 不剥夺条件:资源未释放前,不能被强行夺走;
  3. 请求和保持条件:进程保持当前资源,同时能请求其他资源;
  4. 循环等待条件:存在进程资源循环等待链,链中进程获得的资源同时被下一个进程所请求。

死锁的解决方案

保持上锁顺序一致

处理方法

其他

外中断和异常的区别

程序从开始运行到结束的完整过程

  1. 预编译:处理源代码中以 # 开头的预编译指令,处理规则:
    • 删除 #define,展开所有宏定义
    • 处理所有条件预编译指定,如 #if,#endif,#ifdef, #elif, #else
    • 处理 #include 指令,递归替换文件内容到指令位置
    • 删除注释
    • 保留 #pragma 指令
    • 添加行号和文件标识,便于调试和错误输出
  2. 编译:预编译后文件进行词法分析、语法分析、语义分析及优化,生成汇编文件
    • 词法分析:利用类似“有限状态机”算法,将源码输入到扫描机中,将字符序列分割成一系列记号;
    • 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树;
    • 语义分析:语法分析只是完成对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义;
    • 目标代码生成:将中间代码转换成机器代码,汇编语言标识;
    • 代码优化:寻找合适的寻址方式、使用位移来替换乘法、删除多余指令等。
  3. 汇编
    将汇编代码转换成机器可以执行的机器码文件。经汇编后,产生 xxx.o、xxx.obj 文件
  4. 链接
    将不同的目标文件进行链接,从而形成一个可执行的程序。链接分为静态链接和动态链接:
    • 静态链接:函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。优点是速度快,在可执行程序中具备所有需要的东西;缺点的空间浪费,更新困难。
    • 动态链接:把程序按模块拆分成相对独立部分,在运行时在链接在一起,形成完成过程。(静态链接是把所有模块链接成单独一个可执行文件)。优点:共享库、更新方便,缺点是性能损耗。

局部性原理

linux 中断和异常的区别

相同点:

不同点:

服务器高并发解决方案

  1. 应用数据与静态资源分离。即将静态资源(图片、视频、js 等)单独存储到静态资源服务器;
  2. 利用客户端缓存,页面尽可能用静态来实现,对于更新部分用 ajax 异步请求获取动态数据;
  3. 集群和分布式。集群是所有服务器功能相同,负载均衡。
  4. 反向代理。访问服务器时,通过别的服务器获取资源和结果返回给客户端。

标签:面试题,操作系统,访问,汇总,互斥,线程,内存,进程,资源
来源: https://www.cnblogs.com/cscshi/p/16409672.html