!> 操作系统 面试题以及答案整理
操作系统为了跟踪每个进程的活动状态,维护了一个进程表。进程表的内部列出了每个进程的状态以及每个进程使用的资源等。
进程:正在执行程序的一个实例,是资源分配的基本单位。(进程控制块(process control block)描述进程的基本信息和运行状态,所谓的创建和撤销进程,都是指对PCB的操作) 线程:进程中的单条流向,是程序独立调度的基本单位。
区别:
如果问到为什么进程切换开销大:进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
上面的参考4做的笔记: 每个进程都有自己的虚拟地址空间,但是为了进一步保障系统运行安全,虚拟地址空间被划分为用户空间和内核空间,操作系统运行在内核空间,用户程序运行在用户空间,内核空间由所有进程的地址空间共享(所有进程的内核空间(3G-4G)都是共享的 属于所有进程。内核层共享,只是为了提供内核应该提供的访问接口。这样应用程序才能通过这些空间来和内核交互,以及调用其他功能和访问硬件。),但是用户程序不能直接访问内核空间,操作系统保存的进程控制信息自然是在内核空间,这里除了页目录以外还能找到很多重要的内容,例如进程和父进程ID,状态,打开文件句柄表等等
线程就是进程中的执行体,它要有指定的执行入口,通常是某个函数的指令入口,线程执行时要使用从进程虚拟地址空间中分配的栈空间来存储数据,这被称为线程栈,在创建线程时, 操作系统会在用户空间和内核空间分别分配两段栈,就是我们通常所说的用户栈和内核栈,线程切换到内核态执行时会使用内核栈,为的是不允许用户代码对其进行修改以保证安全,操作系统也会记录每个线程的控制信息(例如执行入口,线程栈,线程id),windows中线程控制信息对应TCB,在PCB中可以找到进程拥有的线程列表,同一个进程内的线程会共享进程的地址空间和句柄表等资源,而在linux中只使用了一个task_struct结构体,进程在创建子进程时会指定它和自己使用同一套地址空间和句柄表等资源,用这种方法来实现多线程的效果,
-> -> -> -> -> ->
如果接下来要执行进程A中的线程a1,执行入口如图所示在那里,cpu的指令指针就会指向线程的执行入口,当前执行用户空间的程序指令,所以栈基和栈指针寄存器会记录用户栈的位置,可以看到程序执行时,cpu面向的是某个线程,所以才说线程是操作系统调度和执行的基本单位,一个进程中至少要有一个线程,它要从这个线程开始执行,这被称为它的主线程,可以认为主线程是进程中的第一个线程,一般是由父进程或者操作系统创建的,而进程中的其他线程,一般都是由主线程创建的,
线程中发生函数调用时就会在线程栈中分配函数调用栈,而虚拟内存分配,文件操作,网络读写等很多功能,都是由操作系统来实现,再向用户程序暴露接口,所以,线程免不了要调用os提供的系统服务,也就是少不了进行系统调用,cpu中会有一个特权级标志,用户记录当前程序是在用户态还是内核态,只有标记为内核态时才可以访问内核空间,而目前线程a1处于用户态,还不能访问内核空间,所以系统调用发生时就得切换到内核态,使用线程的内核栈,执行内核空间的系统函数,这被称为从"用户态"切换到"内核态"。
最初系统调用是通过软中断触发的,所谓软中断,就是通过指令模拟中断,与软中断对应的是硬件中断,操作系统会按照cpu硬件要求,在内存里存一张中断向量表,用来把各个中断编号映射到相应的处理程序,例如Linux系统中,系统调用中断对应的编号为0x80,对应的处理程序就是用来派发系统调用的,为什么说派发系统调用呢?因为操作系统提供了数百个系统调用,不能为每一个都分配一个中断号,所以操作系统又实现了一张系统调用表,用于通过系统调用编号,找到对应的系统函数入口,所以用户程序这里,会把要调用的系统函数编号存入特定寄存器,通过寄存器或用户栈来传递其他所需参数,然后用int 0x80来触发系统调用中断,而硬件层面,CPU有一个中断控制器,它负责接收中断信号,切换到内核态,保存用户态执行现场,一部分寄存器的值会通过硬件机制保存起来,还有一部分通用寄存器的值,被压入内核栈中,然后去中断向量表这里查询0x80对应的系统调用派发程序入口,而系统调用的派发程序会根据指定的系统调用编号,去系统调用表这里查询对应的系统调用入口并执行,后来为了优化系统调用的性能,改为通过特殊指令触发系统调用,例如x86的sysenter,和amd64平台下的syscall,当cpu执行到这些指令时就会陷入内核态,从专用寄存器拿到派发入口的地址,省去了查询中断向量表的过程,等系统调用结束后,再利用之前保存的信息,恢复现场在用户态的执行现场,继续执行后面的指令,这样就完成了一次系统调用。ok,对线程的调用有一个大致了解。
接下来就可以看看线程切换是怎么回事了? 我们知道现代操作系统中,cpu的执行权被划分为不同的时间片,只有获得cpu时间片的程序才可以运行,由于时间片很短,所以用户感觉不到程序的切换过程,又因为cpu很快,所以即使很短的时间片,也足够它执行很多很多的指令,一个线程获得的时间片用完时,cpu硬件时钟会触发一次时钟中断,对应的中断处理程序,会从已经就绪的线程中挑选一个来执行,我们暂不展开线程调度的问题,只关注切换过程。 例如接下来要从线程a1切换到线程a2,这两个线程同属于进程A,那么这就涉及到线程切换, 只需要把线程a1的执行现场保存起来,后续再把指令指针,栈指针这些寄存器的值,修改为线程a2的信息,修改一下内存中调度相关的数据结构,一次同进程间的线程切换就算完成了。
->
等到线程a1再次获得时间片之后,会根据之前保存的信息,恢复到切换前的执行现场,继续完成它的任务,假如线程a1要切换到另一个进程B的线程b1,那么除了线程切换外还有涉及到进程切换,cpu这里保存的页目录地址要切换到进程B,所以进程切换与线程切换的区别就是进程切换会导致地址空间等进程资源发生变化,会导致TLB缓存失效,代价相应的会更大。 ->
到目前为止,我们已经了解了进程和线程的结构,(如下图所示) 理解了线程的用户栈和内核栈,(如下图所示)
以及进程切换与线程切换的大致过程,(如下图所示) ->
不过时间片轮转也只是触发进程或线程切换的多种场景之一,
线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
进程间通信:(Inter process communication) 几种方式:
消息队列:消息队列是内核中存储消息的链表,它由消息队列标识符进行标识,这种方式能够在不同的进程中间提供全双工通信连接
管道:用于两个相关进程间通信,是一种半双工的通信方式,只能在父子进程中使用,如果需要全双工,需要另外的一个管道。
套接字:与其他通信方式不同,可以用于不同机器之间的进程通信。
信号量:是一个计数器,用于为多个进程提供对共享数据对象的访问
共享内存:使用所有进程的内存来建立连接,不过需要同步进程(信号量)来保护访问。是最快的IPC方式。
消息传递:消息传递是进程间实现通信和同步等待的机制。消息直接由发送方传递给接收方。
管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
远程过程调用(RPC)
类似问题: 无名管道,命名管道,共享内存,消息队列,套接字(分别有哪些例子)
类似问题:进程间怎样通信
背景:当两个或两个以上的进程/线程处于就绪状态时,如果只有一个CPU可用,那么必须选择接下来哪个进程/线程可以运行,操作系统中有一个叫做调度程序(scheduler)的角色存在就是做这件事的。
不同的系统中,调度算法是不同的,有如下3种情况:
批处理
交互式
实时
参考自己的os打印的那个笔记书
并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。
并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。
注意点:并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』
一个让人更加容易理解的例子:
共享的资源有:
独享的资源有:
参考:https://blog.cugxuan.cn/2020/07/07/Linux/a-bug-to-learn-orphan-and-zombie/
Unix进程模型中,进程是按照父进程产生子进程,子进程产生子子进程这样的方式创建出完成各项相互协作功能的进程的。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。如果父进程没有这么做的话,会产生什么后果呢?此时,子进程虽然已经退出了,但是在系统进程表中还为它保留了一些退出状态的信息,如果父进程一直不取得这些退出信息的话,这些进程表项就将一直被占用,此时,这些占着茅坑不拉屎的子进程就成为“僵尸进程”(zombie)。系统进程表是一项有限资源,如果系统进程表被僵尸进程耗尽的话,系统就可能无法创建新的进程。
那么,孤儿进程又是怎么回事呢?孤儿进程是指这样一类进程:在进程还未退出之前,它的父进程就已经退出了,一个没有了父进程的子进程就是一个孤儿进程(orphan)。既然所有进程都必须在退出之后被wait()或waitpid()以释放其遗留在系统中的一些资源,那么应该由谁来处理孤儿进程的善后事宜呢?这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程“凄凉地”结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。这样来看,孤儿进程并不会有什么危害,真正会对系统构成威胁的是僵尸进程。那么,什么情况下僵尸进程会威胁系统的稳定呢?设想有这样一个父进程:它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵尸进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(通过kill发送SIGTERM或者SIGKILL信号)。枪毙了元凶进程之后,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经“僵尸”的孤儿进程就能瞑目而去了。
面试官解释说是padding操作,也就是struct中的内存对其那种
死锁一般发生在多线程(两个或两个以上)执行的过程中。因为争夺资源造成线程之间相互等待,这种情况就产生了死锁。我在 06 讲也提到了死锁,但是并没有讲它产生的原因以及怎么避免,所以接下来我们就来了解这部分内容。 案例: 比如你有资源 1 和 2,以及线程 A 和 B,当线程 A 在已经获取到资源 1 的情况下,期望获取线程 B 持有的资源 2。与此同时,线程 B 在已经获取到资源 2 的情况下,期望获取现场 A 持有的资源 1。
那么线程 A 和线程 B 就处理了相互等待的死锁状态,在没有外力干预的情况下,线程 A 和线程 B 就会一直处于相互等待的状态,从而不能处理其他的请求。
并发场景下一旦死锁,一般没有特别好的方法,很多时候只能重启应用。因此,最好是规避死锁,那么具体怎么做呢?答案是:至少破坏其中一个条件(互斥必须满足,你可以从其他三个条件出发)。
持有并等待:我们可以一次性申请所有的资源,这样就不存在等待了。
不可剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。
循环等待:可以靠按序申请资源来预防,也就是所谓的资源有序分配原则,让资源的申请和使用有线性顺序,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样的线性化操作就自然就不存在循环了。
常用的是如下4种:
完整的列表:
后面的问题:内存每次从磁盘换页进来的效率很低,如何优化?
后面的问题:一个大型游戏,哪些数据要加载到内存,哪些数据要存放在磁盘
进程管理,文件管理,内存管理,设备管理,
总结:CPU密集型任务使用多进程,IO密集型任务使用多线程
同步是阻塞模式,异步是非阻塞模式。
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,知道收到返回信息才继续执行下去; 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回式系统会通知进程进行处理,这样可以提高执行的效率。 由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。看下图
虚拟内存是什么?
内存每次从磁盘换页进来的效率很低,怎么优化?
同步 IO 和异步 IO
5、 进程和线程的区别,创建线程和进程的开销指什么
6、 死锁及如何避免
计算机为什么要用补码?
计算机内部的存储结构?虚拟内存和物理内存的区别?
多进程和多线程的区别
进程间的通信,文件通信了解过么?线程间共享的有哪些东西?
操作系统你了解多少,你最常用的系统是哪个,对 shell 命令熟悉吗,如何查看内存和 cpu 使用情况
3.进程和线程的区别
4.进程间有哪些通信方式,如果有两个进程,一个进程执行一半要求另一个进程终止,该 如何操作
linux shell 中按下 ctrl+c 会怎么样,为什么?(发送信号使进程退出)
内核态与用户态区别(记不清了,于是 balabala…提到了通过系统调用与中断来进入内核 态…)
软中断与硬中断(硬中断记得是外设硬件产生,软中断记不得了……)
如何防止密码被 hash 碰撞攻击(加盐,但是我只记得个名字了,内容与***作忘了……)
为什么扫二维码可以实现登录(妈呀这啥)
3.线程和进程之间哪个快,有什么区别
interrupt 与 signal 有什么差别
interrupt 的发起和接受者是谁
操作系统在 interrupt 中发挥了什么作用
signal 呢,发起者又是谁,接收者呢?(这里答得有点混乱)
进程地址空间布局讲一下
BSS 为什么要叫这个名字?(后来查了,block started by symbol)
一个进程,有 10 个子进程,那么一个子进程 fork 一个子进程,那么这个子进程 有多少个进程。
物理地址和虚地址
buffer 和 cacahe 区别
CPU 有哪些组成,你怎么设计一个 CPU?CPU 调度算法有哪些?
虚拟内存是什么,分段分页是啥?
编译有哪些阶段?
信号量 PV 原语写个生产者消费者模型,父亲生产苹果,母亲生产橘子,盘子里面只能放 一个水果,儿子只吃苹果,女儿只吃橘子
问一个数据是怎么写到磁盘上去的,我就随便答了下
线程之间的通信(我回答成了进程之间的,后来又强行答了一波)
linuxIO 模型,区别在哪
线程独立拥有哪些资源
协程和线程有什么差别,优势呢?