【八股cover#4】OS Q&A与知识点
OS Q&A与知识点
重点知识
进程
概念
我们编译的代码可执行文件只是储存在硬盘的静态文件,运行时被加载到内存,CPU执行内存中指令,这个运行的程序被称为进程。
进程是对运行时程序的封装,操作系统进行资源调度和分配的基本单位。
进程的实现
当中断发生后,操作系统会进行一系列底层工作。
首先,硬件会将当前程序的状态信息压入堆栈中,并装入新的程序计数器指向中断处理程序。然后,汇编语言过程会保存寄存器值并设置新的堆栈。接下来,C中断服务例程会被调用运行,通常是读取和缓冲输入。在此期间,调度程序会决定下一个要运行的进程。随后,C例程返回至汇编代码,并开始运行新的当前进程。这些步骤共同完成了中断处理的过程。
中断发生后操作系统底层的工作步骤:
1.硬件压入堆栈程序计数器等
2.硬件从中断向量装入新的程序计数器
3.汇编语言过程保存寄存器值
4.汇编语言过程设置新的堆栈
5.C中断服务例程运行 (典型地读和缓冲输入) 6.调度程序决定下一个将运行的进程
7.C过程返回至汇编代码
8.汇编语言过程开始运行新的当前进程
以上是进程的实现过程,请以面试回答的角度概括上述内容,要求口语化表达,简洁清晰
进程表
为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表。也称为进程控制块,用于存储每个进程的关键信息,如程序计数器、堆栈指针、内存分配情况和打开的文件状态等。这些信息可以帮助操作系统在进程从运行态转为就绪或阻塞态时保存必要的状态,以便恢复该进程的执行。
每个进程占有一个进程表项。(有些著作称这些为进程控制块)
该表项包含了一个进程状态的重要信息
包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号的调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动就像从未中断过一样
进程并发与并行
单个核心在很短的时间内分别执行多个进程,称为并发(CPU需要实现进程间切换,因此需要保存进程的状态信息)
多个核心同时执行多个进程称为并行
进程状态
在进程的状态中,通常有三种状态:运行态、就绪态和阻塞态。
当进程处于阻塞态时,它需要等待某些事件的发生才能继续运行,而这种阻塞行为会浪费内存空间。因此,操作系统可能会把阻塞的进程换出到磁盘上,从而释放实际内存,我们称之为挂起状态。
此外,除了创建和结束状态之外,还有一种新的状态叫做挂起态,用于描述进程没有占用实际物理内存的情况。总的来说,就绪态和运行态可以相互转换,其它状态都是单向转换。阻塞态是由于缺少需要的资源而被迫转换为该状态,但不包括 CPU 时间,而缺少 CPU 时间会从运行态转换为就绪态。
挂起不仅仅可能是物理内存不足,比如sleep系统调用过着用户执行Ctrl+Z也可能导致挂起;
只有就绪态和运行态可以相互转换,其它的都是单向转换;(就绪态的进程通过调度算法从而获得CPU时间,转为运行状态)
运行态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度;
阻塞态是缺少需要的资源从而由运行态转换而来,但是该资源不包括 CPU 时间,缺少CPU 时间会从运行态转换为就绪态。
进程控制块
进程控制块(PCB)是操作系统用来管理和控制进程的数据结构,它包含了进程的进程标识符(唯一标识)、用户标识符(状态、优先级、资源分配清单)等信息,同时也保存了CPU寄存器的值以便于进程切换后能够从断点处继续执行。PCB是通过链表形式组织起来,方便进程管理,比如就绪队列和阻塞队列等。
守护进程
守护进程是指在后台运行的,没有控制终端与它相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等
创建守护进程的要点
(1) 在后台执行程序,可以使用fork()和setsid()函数实现;
(2) 守护进程需要摆脱父进程的影响,可以调用setsid()使进程成为一个会话组长并脱离原来的登录会话、进程组和控制终端;
(3) 禁止进程重新打开控制终端,可以再次调用fork()创建新的子进程,并使其不再是会话组长;
(4) 关闭不再需要的文件描述符以节省系统资源;
(5) 将当前目录更改为根目录以避免继承过来的文件创建屏蔽字引起的权限问题;
(6) 使用unmask()将文件创建屏蔽字清零;
(7) 处理SIGCHLD信号以避免产生僵尸进程。
僵尸进程
在多进程程序中,父进程需要跟踪子进程的退出状态。如果子进程结束后,父进程还没捕获到子进程的退出状态,那么子进程就会变成僵尸进程,占据内核资源。
为了避免这种情况的发生,可以采用一些方法。比如,使用wait/waitpid函数等待子进程结束或者忽略SIGCHLD信号让内核回收子进程资源。同时,非阻塞调用可以提高程序效率。这个问题在并发服务器中是常见的,因为服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,则可让内核把僵尸子进程转交给init进程(内核启动的第一个用户级进程)去处理,省去大量僵尸进程占用系统资源的问题。
多进程
进程结构包括代码段、堆栈段和数据段,其中代码段是静态的二进制代码,可以被多个程序共享。当父进程创建子进程时,它们除了pid外几乎一样。它们共享全部数据,但当子进程写入数据时,会使用写时复制技术来拷贝一份公共的数据,并在新的拷贝上进行操作。如果子进程想要运行自己的代码段,可以调用execv()函数重新加载新的代码段,从而与父进程独立开来。
进程调度算法
![image-20230616105428696](https://cdn.jsdelivr.net/gh/DAYceng/drawingBed/blogImg/image-20230616105428696.png)
在批处理系统中,常用的调度算法有先来先服务、最短作业优先和最短剩余时间优先。
先来先服务是按请求顺序调度,有利于长作业但不利于短作业。
最短作业优先按估计运行时间最短的顺序进行调度,但长作业可能会一直等待短作业执行完毕而饿死。
最短剩余时间优先是最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。
而在交互式系统中,常用的调度算法有时间片轮转调度、优先级调度和最短进程优先。
时间片轮转调度将CPU时间分配给队首进程执行一个时间片,然后转移到下一个进程。
优先级调度按每个进程的优先级进行调度,可以随着时间推移增加等待进程的优先级。
最短进程优先则是优先运行最短的作业以使响应时间最短。
具体如下
1、批处理系统中的调度
(1) 先来先服务: 非抢占式的调度算法,按照请求的顺序进行调度
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行而长作业又需要执行很长时间,造成了短作业等待时间过长(2) 最短作业优先:
非抢占式的调度算法,按估计运行时间最短的顺序进行调度
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度(3)最短剩余时间优先:
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度
当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待2、交互式系统中的调度
(1) 时间片轮转调度 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的未尾,同时继续把 CPU 时间分配给队首的进程
(2) 优先级调度
为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度可以随着时间的推移增加等待进程的优先级。(3)最短进程优先
如果我们将每一条命令的执行看作是一个独立的“作业”,则我们可以通过首先运行最短的作业来使响应事件最短
进程通信
通信的类型
同一主机上:
无名管道、有名管道、信号、消息队列、信号量
不同主机间:
socket
其中,无名管道和有名管道适用于单向通信(半双工),消息队列支持进程间传输数据块,信号量可以用来协调进程对共享资源的访问,而信号则是通知进程发生了某个事件。而 socket 则是不同主机之间进行通信的标准方式,它可以通过 TCP 或 UDP 协议实现数据传输。
无名管道的特点
无名管道是一种用于实现父子进程之间或兄弟进程之间通信的简单机制,数据只能在一个方向上流动,遵循先入先出的原则。它不属于文件系统,存在于内存中,没有名字,只能在有亲缘关系的进程间使用。数据传输无格式约束,读写双方需要事先约定好数据格式。从管道读数据是一次性操作,数据被读取后就会从管道中删除。在数据传输过程中可能会阻塞。
无名管道和有名管道的异同
有名管道和无名管道的不同在于,有名管道提供了一个路径名与之关联,并以文件形式存在于文件系统中。即使创建有名管道的进程不存在亲缘关系,其他进程仍然可以通过访问该路径来进行相互通信。
此外,FIFO 在文件系统中被看作是一个特殊的文件,但其内容实际上存放在内存中,因此当使用FIFO的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。另外,由于FIFO有名字,不相关的进程可以通过打开命名管道进行通信。
消息队列
消息队列是一种进程间通信方式,其基本原理是,一个进程向消息队列中放入数据后,可以立即返回,而等待读取该消息的进程则可以随时读取。
消息队列实际上是保存在内核中的消息链表,每个消息体都有固定大小的存储块。其中,读取过的消息会被内核删除。
然而,消息队列的缺点在于通信不及时,传输数据有大小限制,并且存在用户态与内核态之间的数据拷贝开销。
信号
信号是一种linux进程通信的方式,也是最古老的形式。它是在软件层面上对中断机制的一种模拟,可以用来处理突发事件。信号具有简单、不能携带大量信息和需要满足特定条件才发送的特点。通过信号,内核进程可以通知用户空间进程发生了哪些系统事件,是一种方便快捷的进程间通讯方式。
一个完整的信号周期:信号先产生,然后再进程中注册/注销并执行对应的信号处理函数
一些比较重要的信号:
SIGINT 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号,终止进程
SIGQUIT 用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号,终止进程
SIGSEGV 指示进程进行了无效内存访问(段错误),终止进程并产生core文件SIGPIPE Broken pipe向一个没有读端的管道写数据,终止进程
SIGCHLD 子进程结束时,父进程会收到这个信号,忽略这个信号
守护进程
守护进程是Linux中的后台服务进程,通常用于周期性执行任务或等待处理某些事件。为了避免被终端信息打断,它通常独立于控制终端运行,并采用以d结尾的名字。守护进程存在于etc/init.d目录下,是一种特殊的孤儿进程。在Linux服务器中,大多数服务都是通过守护进程来实现的。
守护进程模型
创建守护进程的过程包括以下几个必须步骤和一些可选步骤。必须步骤如下:
1.创建子进程,父进程退出,使工作完全在子进程中进行并脱离控制终端。
2.在子进程中创建新会话,使用setsid()函数,使子进程完全独立出来,脱离控制。
3.开始执行守护进程核心工作,即守护进程退出处理程序模型。
可选步骤如下:
1.改变当前目录为根目录,使用chdir()函数,防止占用可卸载的文件系统。
2.重设文件权限掩码,使用umask()函数,防止继承的文件创建屏蔽字拒绝某些权限,并增加守护进程的灵活性。
3.关闭文件描述符,避免继承的打开文件浪费系统资源,无法卸载。
线程
线程的特点
线程是轻量级进程,可以看作是进程的一部分。
在Linux内核中,进程和线程都拥有自己的PCB(进程控制块)。创建线程使用的底层函数和进程相同,都是调用内核函数clone。如果复制对方的地址空间,就产生一个进程,如果共享对方的地址空间,就产生一个线程。在Linux内核中,进程和线程是没有区别的,只是在用户层面进行区分,因此线程所有操作函数都是库函数而不是系统调用。总结来说,线程是最小的执行单位,而进程是最小的分配资源单位。
线程的实现
线程的实现可以分为三种方式:用户线程、内核线程和轻量级线程(LWP)。
在用户线程中,线程库负责管理调度,不需要内核直接参与,因此可以用于不支持线程技术的操作系统。但是一旦某个用户线程阻塞,导致整个进程下的所有用户线程都无法运行,同时用户线程的创建、终止、同步、调度等都不是由操作系统直接参与。
在内核线程中,由操作系统管理、调度,CPU也直接分配给内核线程,但需要占用内核资源进行维护,同时开销比较大。
在轻量级线程中,用户线程像普通进程一样被调度,每个LWP都需要一个内核线程的支持,实际上用户线程是运行在LWP上的。
线程共享与非共享资源
在多线程编程中,一些资源可以被多个线程共享,包括文件描述符表、信号的处理方式、当前工作目录、用户ID和组ID等。而另外一些资源是线程私有的,比如线程ID、处理器现场和栈指针、独立的栈空间以及 errno 变量等。此外,每个线程也有自己的信号屏蔽字和调度优先级。
线程共享资源
文件描述符表每种信号的处理方式当前工作目录
用户ID和组ID
线程非共享资源
线程id处理器现场和栈指针(内核栈)独立的栈空间(用户空间栈)errno变量
信号屏蔽字
调度优先级
线程的优缺点
优点:
1.提高程序并发性
2.开销小
3.数据通信、共享数据方便
缺点:
1.库函数,不稳定
2.调试、编写困难、gdb不支持
3.对信号支持不好
如何减少线程开销?
尽可能地共享资源:线程创建后会共享其所属进程的资源管理信息,因此在设计时应尽量减少线程自身独享的资源,并且合理利用共享内存等机制,提高线程间通信效率。
优化线程的生命周期:线程的创建和销毁都需要一定的开销,在使用线程时应尽量避免频繁地创建和销毁线程,而是重复利用已有的线程。
合理设置线程的优先级:线程优先级的不合理设置可能导致某些线程一直占用CPU,从而影响其他线程的执行。因此,在实际应用中应该根据实际情况对线程进行合理的优先级设置。
避免死锁和竞态条件:死锁和竞态条件可能导致线程无法正常执行,从而浪费系统资源。因此,在多线程编程时应该注意避免这些问题的发生。
线程通信
线程通信的主要目的是为了线程同步,而不是数据交换。由于在同一进程中的不同线程共享同一份内存区域,因此线程之间可以通过复制数据到共享变量中来方便、快速地共享信息。但需要注意的是,必须避免多个线程试图同时修改同一份信息,否则会出现问题。
多线程的好处
很多应用中有可能同时发生多个活动,将这些应用程序分解成并发运行的多个线程,达到简化设计模型的目的。与多进程相比,多线程开销更小、更容易创建和释放。
此外,当多个线程是IO密集型时,多线程可以使这些活动彼此重叠运行,从而加快程序执行的速度。因此,多线程适用于需要同时处理多个任务并且有共享数据和地址空间的情况。
对于线程需要考虑的是
线程之间有无先后访问顺序(线程依赖关系)
多个线程共享访问同一变量(同步互斥问题)
同一进程的多个线程共享进程的资源,除了标识线程的tid,每个线程还有自己独立的栈空间,线程彼此之间是无法访问其他线程栈上内容的。
协程
协程(Coroutine)是一种比线程更加轻量级的并发解决方案。与传统的线程或进程不同,协程可以在单线程内完成并发任务而不需要线程间的切换开销。协程通过在函数执行过程中暂停并保存当前状态,在下次继续执行时从暂停的地方恢复来实现。
C++20引入了协程(Coroutine)的原生支持。C++中的协程通过co_await和co_yield关键字实现,可以让函数在执行过程中暂停并保存当前状态,在下次继续执行时从暂停的地方恢复。在C++20之前,可以使用第三方库来实现协程(例如Boost.Coroutine、CoroutineTS等)
Q&A
进程线程的区别,系统调用是进程还是线程
进程是操作系统中进行资源分配和调度的基本单位,线程是在进程内执行的实体,它们共享进程的资源。一个进程可以包含多个线程。
系统调用通常是由进程发起的,因为进程是操作系统中最基本的调度单位,系统调用需要使用操作系统提供的服务来完成特定任务,如读写文件、网络通信等。线程只是进程内部的一种执行实体,不具备独立地发起系统调用的能力,其所需的系统调用都必须由其所属的进程发起。
异步IO与同步IO的区别
同步IO和异步IO的主要区别在于IO操作是否会阻塞进程。
同步IO操作会阻塞进程,直到IO操作完成并返回结果,而异步IO操作则不会阻塞进程,而是在IO操作执行完毕后,通过回调函数等方式通知进程IO操作已经完成,并返回结果。
因此,异步IO操作可以提高程序的执行效率和吞吐量,尤其在处理大量IO密集型任务时更为明显。而同步IO操作更适用于少量且时间敏感的任务,比如用户输入等需要立即得到响应的场景。
为什么系统调用比较消耗CPU?
系统调用比较消耗CPU主要是因为涉及到用户态和内核态之间的切换。这个过程中进行了多次上下文切换、状态保存和恢复等操作,因此会消耗大量的CPU时间,降低系统性能。
为什么要使用多线程?
使用多线程可以提高程序的运行效率和响应速度。通过同时执行多个任务,可以充分利用 CPU 的计算能力,避免单线程情况下的资源浪费和阻塞等问题。此外,多线程还可以提高程序的可扩展性和并发性,在处理大量数据或复杂业务逻辑时特别有用。
多进程与多线程的优缺点及适应场景
多进程的优点:
增加了系统并发能力:可以利用多个CPU同时处理不同的任务,从而提高系统吞吐量和响应速度。
更加稳定:不同的进程之间相互独立,一个进程崩溃不会影响其他进程的运行。
安全性高:进程间通信需要使用操作系统提供的IPC机制,这样可以保证数据传输的安全性。
多进程的缺点:
开销较大:进程之间切换需要保存和恢复上下文信息,切换时需要频繁的调度,因此开销比较大。
编程模型复杂:进程之间通信需要使用操作系统提供的IPC机制,编程难度较大。
适应场景:
多进程适合于以下场景
CPU密集型任务:多进程可以利用多个CPU同时处理不同的任务,从而提高系统吞吐量和响应速度。
对稳定性要求较高的任务:不同的进程之间相互独立,一个进程崩溃不会影响其他进程的运行,因此适用于对稳定性要求高的任务。
========================================================================================================
多线程的优点:
资源开销小:线程之间切换的开销比进程之间切换的开销小得多。
编程模型简单:线程共享进程的资源,因此通信和同步机制相对简单。
可以充分利用CPU时间片:线程可以共享CPU的执行时间,从而提高系统的并发能力。
多线程的缺点:
容易出现问题:由于线程共享进程的资源,如果没有良好的同步机制,就会出现资源竞争或死锁等问题。
可靠性较差:一个线程崩溃可能会导致整个进程崩溃。
适应场景:
多线程适合于以下场景
IO密集型任务:多线程可以充分利用CPU时间片,提高系统的并发能力,从而提高IO密集型任务的响应速度。
对资源要求较高的任务:线程共享进程的资源,可以节省系统资源,因此适用于对资源要求较高的任务。
多线程开发的注意事项
多线程开发的注意事项如下:
- 线程安全问题:需要注意共享资源的读写顺序和同步机制,避免出现数据竞争、死锁等问题。
- 上下文切换问题:线程之间的切换会产生上下文切换开销,如果线程数量过多,会影响系统性能。
- 内存管理问题:多线程程序中需要注意内存分配和释放的线程安全性,防止出现内存泄漏或者重复释放等问题。
- 性能调优问题:需要根据具体的应用场景进行性能调优,如线程池的大小、任务队列的长度等。
- 异常处理问题:需要注意异常处理的方式和机制,避免线程崩溃导致整个进程崩溃。
- 调试问题:多线程程序的调试比较困难,需要使用调试工具进行分析和定位问题。
- 可维护性问题:多线程程序的可维护性比较差,因为程序逻辑比较复杂,需要注重代码的可读性和清晰度。
为什么要使用线程池?线程池有什么优点?(线程开销)
使用线程池可以避免频繁创建和销毁线程的开销,提高程序的性能和资源利用率。
线程池有以下优点:
节约线程开销:线程池中的线程可以被重复利用,避免了线程的频繁创建和销毁,减少了线程开销。
提高响应速度:线程池可以立即处理任务,无需等待线程的创建和启动过程,从而提高了程序的响应速度。
优化资源利用:线程池可以根据系统负载情况自动调整线程数量,充分利用系统资源,避免资源浪费。
提高程序稳定性:线程池可以限制同时执行的任务数量,避免因过多的并发请求导致系统崩溃或资源耗尽的问题。
线程切换的到底是什么?进程切换为何比线程慢?
线程切换是指 CPU 从当前执行的线程切换到另一个等待执行的线程的过程。在多线程程序中,当某个线程需要等待 I/O 操作或者其他操作时,CPU 可以切换到另一个就绪状态的线程进行执行,从而提高程序的并发能力。
进程切换是指 CPU 从当前执行的进程切换到另一个等待执行的进程的过程。相比线程切换,进程切换涉及到内存映射和页表的切换,这些操作需要耗费较长的时间。而线程之间共享同一进程的资源,所以在线程切换时只需要切换少量的上下文信息,比进程切换快得多。
综上所述,线程切换是指 CPU 在多线程程序中从当前线程切换到另一个就绪状态的线程,而进程切换则是从当前进程切换到另一个就绪状态的进程。由于进程切换需要保存和恢复更多的上下文信息,因此比线程切换慢。