面试的过程中,为了考察面试者的基础功力,除了算法以外,操作系统将会占比很大的权重,本文给大家分享我在面试过程中出现的非常高频的面试题,我基本上会从两个角度来阐述,一个是"官话",一个是大白话。希望对即将面试的你有所帮助

【操作系统突击】1 10道经典操作系统面试题_共享内存


[toc]

1、为什么有了进程,还要有线程呢?

为了提高系统资源的利用率和系统的吞吐量,通常进程可让多个程序并发的执行,但是也会带来一些问题

官话

  • 进程如果在执行的过程被阻塞,那这个进程将被挂起,这时候进程中有些等待的资源得不到执行:
  • 进程在同一时间只能做一件事儿

基于以上的缺点,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时间和空间开销,提高并发性能。

举个例子

小Q当年开发了一个聊天软件,给女朋友说:咋们以后不用什么qq,微信了,我写个聊天工具,咱两正儿八经的两人世界可好。

说干就干,小Q很快就完成了开发,然后开始测试,打电话让女朋友让她发个信息,小Q就等着,等啊等啊,怎么还没有发信息过来,没显示啊,一脸懵逼,单步调试吧,发现程序卡在了wati for user input不动了。牛逼,程序不输入就没办法执行后面的任务。咋搞?要不设置个1s,用户1s不输入直接跳过进行后面的接收阶段显示阶段,牛皮牛皮,果然好使,好使个锤锤,这样用户输入信息不就很可能丢失,咋搞?

能不能将输入和显示这两个动作给分开,一个负责输入,发送消息,一个负责读信息和显示。不夸夸你自己吗,直接开干呈现两个窗口。

回来回来,这就是我们的多进程。不过多进程也还是有些问题需要注意,开多个窗口没问题,无脑开窗口撩骚直接被榨干(内存耗尽),而且想要几个窗口交换个数据也是贼麻烦,这是为啥呢

多进程的程序,每个进程都有自己的独立内存空间,都穿了衣服,不能相互乱看,要想通信就要接触系统层面来通信,所以肯定就会造成较多的资源消耗和时间浪费。怎么整?

几个进程为了方便,干脆商量一波,能不能开辟一块内存空间,共乐其中,这就是线程非常重要的意义,不过共享了不代表我们就是""的,个人保密还是要做到,也不要吵架,可不可以通过锁的方式保密呢,这就涉及到了线程的同步。这样我猜测你应该了解进程和线程了吧。

2、简单说下你对并发和并行的理解?

官话从概念出发:

  • 并发

在一个时间段中多个程序都启动运行在同一个处理机中

  • 并行

假设目前A,B两个进程,两个进程分别由不同的 CPU 管理执行,两个进程不抢占 CPU 资源且可以同时运行,这叫做并行。

例子
并发是指多个任务在一段时间内发生。比如今日成都某家老火锅店做活动,全场5折(这还是比较狠),但是只有200个位置,但是来的人太多了,来了250个人,此时多出的50个人只好等待着或者去另外家火锅店。那么火锅店老板对这250的安排不是同一时刻安排而是一段时间去处理,其实这就是并发。这个例子好像整的不算生动,我们再来一个

到了周末就是我们"开黑"的时光,奈何到了周一不迟到怎么对得起自己,但是迟到了被逮住就要被"BB"。好嘛,我们作为学生娃儿只好认怂,常规操作,两脚一登,起床,刷牙,上厕所,拿包包。就是这样类似复读机的习惯操作是怎么回事?莫非我们就能同时干这么多事?其实不是的,我们大脑下达指令,起床,刷牙这些操作早形成了肌肉记忆,所以我们在这小段时间完成了这么多事儿,还可以多几分钟出来看看美女不香?

【操作系统突击】1 10道经典操作系统面试题_多进程_02

平时玩儿电脑的时候,边写代码边听音乐,计算机同时处理了这么多任务。如果是单核 CPU ,在我们看来这些事儿是同时发生的,其实那是因为底层 CPU 切换的速度太快以致于我们完全感受不到它的切换,仅此为错觉而已。但是如果是多核 CPU ,各个 CPU 负责不同的进程,各个进程不抢占 CPU ,这样同时进行,这就是真正意义上的并行

【操作系统突击】1 10道经典操作系统面试题_多进程_03

说了这么多,那并行和并发到底啥区别?
两者区别在于是否"同时"发生。是在一段时间同时发生还是多个事情在同一个时间点同时发生。

3、同步、异步、阻塞、非阻塞的概念

首先大家应该知道同步异步,阻塞非阻塞是两个不同层面的问题,一个是operation层面,一个是kernal层面。同步异步最大的区别在于是否需要底层的响应再执行。阻塞非阻塞最大的区别在于是否立即给出响应。

官话同步:当一个同步调用发出后,调用者要一直等待返回结果。通知后,才能进行后续的执行。

异步:当一个异步过程调用发出后,调用者不能立刻得到返回结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

阻塞:是指调用结果返回前,当前线程会被挂起,即阻塞。

非阻塞:是指即使调用结果没返回,也不会阻塞当前线程。

形象比喻

  • 小Q去钓鱼,抛完线后就傻傻的看着有没有动静,有则拉杆(同步阻塞)
  • 小Q去钓鱼,拿鱼网捞一下,有没有鱼立即知道,不用等,直接就捞(同步非阻塞)
  • 小Q去钓鱼,这个鱼缸比较牛皮,扔了后自己就打王者荣耀去了,因为鱼上钩了这个鱼缸带的报警器会通知我。这样实现异步(异步非阻塞)

4、进程和线程的相关题

官话

  • 进程:进程是系统进行资源分配和调度的一个独立单位,是系统中的并发执行的单位。
  • 线程:线程是进程的一个实体,也是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,有时又被称为轻量级进程。
  • 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位;
  • 创建进程或撤销进程,系统都要为之分配或回收资源,操作系统开销远大于创建或撤销线程时的开销;
  • 不同进程地址空间相互独立,同一进程内的线程共享同一地址空间。一个进程的线程在另一个进程内是不可见的;
  • 进程间不会相互影响,而一个线程挂掉将可能导致整个进程挂掉;

别背了,我知道你们都会背,举个例子

计算机中的核心是 CPU ,说它是我们的大脑一点不为过。在此用一个连锁火锅店举例,因为疫情的影响,今天到目前营业额一般,只好开一家店,其他疫情较为严重的店只好先关闭,这里涉及的含义:单个 CPU 一次只运行一个任务。进程就类似这家火锅店,它代表 CPU 所能处理的单个任务,其他地方火锅店只能处于非运行状态。
一个火锅店有很多服务员,一起协同工作,将火锅店做的红红火火,那么线程就好比这些服务员,一个进程可有多个线程。、
火锅店各个工作房间是共享的,所有服务员都可以进出,这意味着进程的内存空间共享,每个线程都可以使用这些共享内存。但是不是每个房间都可以容纳相同的人数,比如卫生间就只有一个人,其他人不能同时进去。这意味着一个线程在使用共享内存的时候,其他线程需要等待结束才能使用这块内存。那怎么防止别人进入呢?很直接的办法就是进去之后记得关门(上锁),上了锁,其他人想进来发现是上锁了,那就等锁打开后再进去,这就叫做互斥锁,防止多个线程同时读写某一块内存区域。
ok到这里,我们总结一波

  • 如果以多进程的方式运行,那么允许多个任务同时运行
  • 如果以多线程的方式运行,那么允许将单个任务分成不同的部分运行
  • 为了防止进程/线程之间产生冲突和允许进程之间的共享资源,需要提供一种协调机制。

多线程与多进程的基本概念

不知道大家经历过食堂打菜的场景没有,如果学校食堂就开设一个窗口,打菜的阿姨也没办法,只好一个个给大家依次打菜,这就好比"单线程",效率非常低(此处可以考虑为什么redis使用单线程却这么牛逼)
为了提高效率,在食堂多加了几个窗口,这就类似"多线程"形式。
那么又想起一个问题,“多线程一定就比单线程效率高麦?”(ps Memcache是多线程模型而Redis是单线程模型)

貌似我们一提到高并发,分布式,就不得不想起多线程,那么多线程一定比单线程效率高?

上面说了采用多线程多核效果更好,但是多线程对 CPU ,内存等都要求比较高,如果存在的上下文切换过于耗时,互斥时间太久,效率反而会低。
不一定。我不从专业术语来将,举个例子,假设目前接水房有四个水管可以接水,我如果用4个桶分别对应4个水管,那么就比较完美,如果少一个则闲置一个,多一个则会出现抢占。如果此时我的水桶个数大于水管数,为了每个桶都有水,我们就需要切换水桶,这个过程实际上就是线程的上下文切换,代价一样不小。

多线程与多进程的应用场景

多线程的优点

  • 更加高效的内存共享。多进程下内存共享不便
  • 较轻的上下文切换。因为不用切换地址空间,CR3寄存器和清空TLB

多进程的优点:

  • 各个进程有自己内存空间,所以具有更强的容错性,不至于一个集成crash导致系统崩溃
  • 具有更好的多核可伸缩性,因为进程将地址空间,页表等进行了隔离,在多核的系统上可伸缩性更强

如何提升多线程的效率

  • 尽量使用池化技术,也就是线程池,从而不用频繁的创建,销毁线程
  • 减少线程之间的同步和通信
  • 通过Huge Page的方式避免产生大量的缺页异常
  • 避免需要频繁共享写的数据

5、进程的状态转换

在Linux中,进程的状态有七种

  • 可运行状态

英文名词为TASK_RUNNING,其实这个状态虽然是RUNING,实际上并不一定会占有 CPU ,可能修改TASK_RUNABLE会更妥当。TASK_RUNGING根据是否在在 CPU 上运行分为RUNGING和READY两种状态。处于READY状态的进程随时可以运行,只不过因为此时 CPU 资源受限,调度器没选中运行

【操作系统突击】1 10道经典操作系统面试题_共享内存_04

  • 可中断睡眠状态与不可中断睡眠状态

我们知道进程不可能一直处于可运行的状态。假设A进程需要读取磁盘中的文件,这样的系统调用消耗时间较长,进程需要等待较长的时间才能执行后面的命令,而且等待的时间还是不可估算的,这样的话进程还占用 CPU 就不友好了,因此内核就会将其更改为其他的状态并从 CPU 可运行的队列移除。

Linux中存在两种睡眠状态,分别为:可中断的睡眠状态和不可中断的状态。两者最大的区别为是否响应收到的信号,那么从可中断的睡眠的进程是如何返回到可运行的状态呢

  • 等待的事情发生且继续运行的条件满足
  • 收到了没有被屏蔽的信号

处于此状态的进程,收到信号会返回EINTR给用户空间。开发者通过检测返回值的方式进行后续逻辑处理

但是对于不可中断的睡眠状态,就只有一种方式返回到可运行状态,即等待事情发生了继续运行

【操作系统突击】1 10道经典操作系统面试题_多线程_05

上图中为什么出现个 TASK_UNINTERRUPTIBLE 状态,主要是因为内核中的进程不是什么进程都可以被打断,假设响应的是异步信号,程序在执行的过程中插入一段用于处理异步信号的而流程,原来的流程就会被中断。所以当进程在和硬件打交道的时候,需要使用 TASK_UNINTERRUPTIBLE 状态将进程保护起来,从而避免进程和设备打交道的过程中被打断导致设备处于不可控的状态。

那么TASK_UNINTERRUPTIBLE状态会出现多久呢?

其实 TASK_UNINTERRUPTIBLE 状态是很危险的状态,因为它刀枪不入,你无法通过信号杀死这样一个不可中断的休眠状态,正常情况,TASK_UNINTERRUPTIBLE状态存在时间很短,但是不排除存在此状态进程比较持久的情况,真的刀枪不入了?可不可以进行提前的预防?
可以的,早就考虑了。内核提供了hung task机制,它会启动一个khungtaskd内核线程对TASK_UNINTERRUPTIBLE状态进行检测,不能让他失控了。khungtaskd会定期的唤醒,如果超过120s都还没有调度,内核就会通过打印警告和堆栈信息。当然,不一定就是120s,可以通过下面选项进行定制

sysctl kernel.hung_task_timeout_secs

说了这么多,我们怎么知道到底有没有出现这个状态,哪里看?可以通过/proc和ps进行查看

  • 睡眠进程和等待队列

不管是上面提到的可中断的睡眠进程还是不可中断的睡眠进程,都离不开一种数据结构---队列。哦?假设进程A因为某某原因需要休眠,为啥要休眠,等待的资源迟迟拿不到或者等待的事件总是不来,没法进行下一步操作,这个时候内核来了,"行吧,我不会抛弃你,我一定会想办法让你和等待的资源(事件)扯上关系",只要等待的时机到来我就唤醒你,这采用的方法即"等待队列"。如果进一步深究,想了解它的底层实现(采用了双向链表),文末我会给大家推荐基本书籍。

  • TASK_STOPPED状态于TASK_TRACED状态

TASK_STOPPED状态属于比较特殊的状态,可以通过SIGCONT信号回复进程的执行

【操作系统突击】1 10道经典操作系统面试题_共享内存_06

TASK_TRACED是被跟踪的状态,进程会停下来等待跟踪它的进程对它进行进一步的操作

  • EXIT_ZOMBIE状态与EXIT_DEAD状态

当进程储于这两种的任意一种,就可以宣布"死亡" 。

【操作系统突击】1 10道经典操作系统面试题_共享内存_07就绪 —> 执行:准备就绪,调度器满足了的需求,给我一种策略,我就可从就绪变为执行的状态;

执行 —> 阻塞:不是每个进程都是那么一帆风顺,就像我们每次考试,不管是中考高考还是考研,难免都会出现磕磕盼盼,遇到了可能暂时会阻挡我们前行的小事儿,可是要相信不会一直的阻挡我们,只要我们有恒心坚持,时机到来,你也准备好了,那就美哉。回到这里,对于进程而言,当需要等到某个事情发生而无法执行的时候,进程就变为阻塞的状态。比如当前进程提出输入请求,如进程提出输入/输出请求,进程所申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;

阻塞 —> 就绪:处于阻塞状态的进程,在其等待的事件已经发生,如输入/输出完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入执行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为执行状态;

执行 —> 就绪:正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。

6、进程间的通信方式有哪些?

管道

学习软件工程规范的时候,我们知道瀑布模型,在整个项目开发过程分为多个阶段,上一阶段的输出作为下一阶段的输入。各个阶段的具体内容如下图所示

【操作系统突击】1 10道经典操作系统面试题_共享内存_08

最初我们在学习Linux基本命令使用的时候,我们经常通过多个命令的组合来完成我们的需求。比如说我们想知道如何查看进程或者端口是否在使用,会使用下面的这条命令

netstat -nlp | grep XXX

这里的"|"实际上就是管道的意思。"|"前面部分作为"|"后面的输入,很明显是单向的传输,这样的管道我们叫做"匿名管道",自行创建和销毁。既然有匿名管道,应该就有带名字的管道"命名管道"。如果你想双向传输,可以考虑使用两个管道拼接即可。
创建命名管道的方式

mkfifo test

test即为管道的名称,在Linux中一切皆文件,管道也是以文件的方式存在,咋们可以使用ls -l 查看下文件的属性,它会"p"标识。

【操作系统突击】1 10道经典操作系统面试题_多线程_09

下面我们向管道写入内容

echo "666" > test

【操作系统突击】1 10道经典操作系统面试题_多进程_10

此时按道理来说咋们已经将内容写入了test,没有直接输出是因为我们需要开启另一个终端进行输出(可以理解为暂存管道)

cat < test

ok,我们发现管道内容被读出来,同时echo退出。那么管道这种通信方式有什么缺点?我们知道瀑布模型的软件开发模式是非常低下的,同理采用管道进行通信的效率也很低,因为假设现在有AB两个进程,A进程将数据写入管道,B进程需要等待A进程将信息写完以后才能读出来,所以这种方案不适合频繁的通信。那优点是什么?

最明显的优点就是简单,我们平时经常使用以致于都不知道这是管道。鉴于上面的缺点,我们怎么去弥补呢?接着往下看
消息队列

管道通信属于一股脑的输入,能不能稍微温柔点有规矩点的发送消息?

答:可以的。消息队列在发送数据的时候,按照一个个独立单元(消息体)进行发送,其中每个消息体规定大小块,同时发送方和接收方约定好消息类型或者正文的格式。
在管道中,其大小受限且只能承载无格式字节流的方式,而消息队列允许不同进程以消息队列的形式发送给任意的进程。
但是当发送到消息队列的数据太大,需要拷贝的时间也就越多,所以还有其他的方式?继续看

共享内存

使用消息队列可以达到不错的效果,但是如果我们两个部门需要交换比较大的数据的时候,一发一收还是不能及时的感知数据。能不能更好的办法,双方能很快的分享内容数据,答:有的,共享内存

我们知道每个进程都有自己的虚拟内存空间,不同的进程映射到不同的物理内存空间。那么我们可不可以申请一块虚拟地址空间,不同进程通过这块虚拟地址空间映射到相同的屋里地址空间呢?这样不同进程就可以及时的感知进程都干了啥,就不需要再拷贝来拷贝去。
我们可以通过shmget创建一份共享内存,并可以通过ipcs命令查看我们创建的共享内存。此时如果一个进程需要访问这段内存,需要将这个内存加载到自己虚拟地址空间的一个位置,让内核给它一个合法地址。使用完毕接触板顶并删除内存对象。
那么问题来了,这么多进程都共享这块内存,如果同时都往里面写内容,难免会出现冲突的现象,比如A进程写了数字5,B进程同样的地址写了6就直接给覆盖了,这样就不友好了,怎么办?继续往下看

信号量

为了防止冲突,我们得有个约束或者说一种保护机制。使得同一份共享的资源只能一个进程使用,这里就出现了信号量机制。

信号量实际上是一个计数器,这里需要注意下,信号量主要实现进程之间的同步和互斥,而不是存储通信内容。
信号量定义了两种操作,p操作和v操作,p操作为申请资源,会将数值减去M,表示这部分被他使用了,其他进程暂时不能用。v操作是归还资源操作,告知归还了资源可以用这部分。

信号

从管道----消息队列-共享内存/信号量,有需要等待的管道机制,共享内存空间的进程通信方式,还有一种特殊的方式--信号

我们或许听说过运维或者部分开发需要7 * 24小时值守(项目需要上线的时候),当然也有各种监管,告警系统,一旦出现系统资源紧张等问题就会告知开发或运维人员,对应到操作系统中,这就是信号
在操作系统中,不同信号用不同的值表示,每个信号设置相应的函数,一旦进程发送某一个信号给另一个进程,另一进程将执行相应的函数进行处理。也就是说把可能出现的异常等问题准备好,一旦信号产生就执行相应的逻辑即可。

套接字

上面的几种方式都是单机情况下多个进程的通信方式,如果我想和相隔几千里的小姐姐通信怎么办?

这就需要套接字socket了。其实这玩意随处可见,我们平时的聊天,我们天天请求浏览器给予的响应等,都是这老铁。

小结

分享了一下几种进程间通信方式,希望大家能知其然并知其所以然,机械式的记忆容易忘记哦。

  • 管道
  • 消息队列
  • 共享内存
  • 信号量
  • 信号
  • 套接字