进程
什么是进程?
进程是计算机的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位.
进程通信有哪些方式?
- 管道/匿名管道(Pipes):有亲缘关系的父子进程或兄弟进程间的通信,只存在于内存中的文件
- 有名管道(Named Pipes):严格遵循FIFO原则,存在于磁盘介质或文件系统,可以实现对本机任意两个进程的通信。
- 信号(Signal):一种比较复杂的通信方式,用于通知接收进程:某个事件已经发生。
- 消息队列(message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列符标识,遵循先进先出原则,存放在内核中。
只有通过内核重启,或显示删除一个消息队列时,该消息队列才会被真正删除。消息队列可以实现消息的随机查询,消息不一定以先进先出的次序读取,也可以按消息的类型读取。比FIFO更有优势。消息队列克服了信号承载量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - 信号量(semophores):信号量是一个计数器,用于多进程共享数据的访问,信号量的意图在于进程间同步。主要用来解决与同步相关的问题并避免竞争条件。
- 共享内存(shared memory):使得多个进程可以访问同一块内存空间,一个进程可以及时查看到对方进程对共享内存中数据的更新。这种方式需要依靠某种同步机制,比如互斥锁和信号量等。可以说是最有用的进程间通信方式。
- 套接字(Sockets): 这种方式主要用于客户端和服务器之间通过网络进行通信。套接字是支持TCP/IP网络通信的基本操作单元,套接字中的相关函数完成通信过程。
进程间通信的机制有哪些优缺点?如何选择合适的通信机制?
- 管道
管道是单向的,先进先出,提供了简单的流控制,进程读空管道或者写满管道,都会造成进程阻塞。管道分为无名管道和有名管道,前者用于父子进程通信,后者用于任意两个进程通信 - 信号
信号产生的条件:按键,硬件异常,进程调用kill函数将信号发送给另一个进程,用户调用kill命令将信号发给其他进程,传递的消息量比较少,主要是用来通知消息。 - 消息队列
是一个消息链表,可以把消息看作一个记录,具有特定的格式,进程可以向队列中添加消息或读取消息,有缓冲区。 - 共享内存
就是映射一段可以被其他进程访问的内存,这段共享物理内存由一个进程创建,但是多个进程都可以访问,共享内存是进程间共享数据的一种最快的方法。 - 信号量
主要用于保护临界资源,进程可以根据它的值来判断是否能访问某些公共资源,除了用于访问控制外,还可以用于进程同步,相当于计数器。 - 套接字
- 流式套接字
提供可靠的,面向连接的通讯流 - 数据包套接字
定义一种无连接的服务,通过相互独立的报文进行传输,是无序的 - 原始套接字
用于新的网络协议的测试
- 流式套接字
什么是远程过程调用(RPC)?它与进程间通信有何异同?
RPC(Remote Procedure Call)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
相同点:都是进程间通信。
不同点:
1. 调用方式:RPC是基于远程过程调用,它允许一个进程像调用本地进程一样调用远程计算机的进程。而IPC是基于本地。
2. 通信开销:RPC引入更多通信开销,因为它需要在不同计算机之间实现网络通信
3. 透明性:RPC提供透明性,即远程过程调用的实现细节对调用者来说是透明的,而IPC则更多需要考虑本地实现细节。
什么是死锁?死锁的条件是什么?如何避免死锁?
-
死锁(Deadlock)描述的这样一种情况:多个进程/线程同时被阻塞,它们中的一个或全部都在等待某个资源被释放。由于进程,线程被无限期的阻塞,因此程序无法正常终止。
例子:
-
死锁的条件:
- 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。
- 占有并等待:一个进程至少应该占有一个资源,并等待另一个资源,而该资源被其他进程所占有。
- 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源所有权才会被释放。
- 循环等待:有一组等待进程{p0, p1, p2,...pn},p0等待的资源被p1占有,p1等待的资源被p2占有,.....pn资源被p0占有。
Note: 必要条件,缺一不可。
-
C++相关
- 什么是锁?有什么作用?
在c++中,锁是一种同步工具,用于保护共享资源,防止多个线程同时访问,从而避免数据竞争和不一致 - 有哪些锁:互斥锁,自旋锁,信号量和条件变量等
- 互斥锁如何使用
C++11之前,Linux下使用pthread库中的mutex:
#include<pthread.h> pthread_mutex_t mutex_ = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex_); ... phtread_mutex_unlock(&mutex_);
C++11之后引入了std::mutex,统一了各个平台上互斥锁的使用
#include<mutex> std::mutex mutex_; mutex_.lock(); ... mutex_.unlock();
-
pthread_mutex和std::mutex有没有非阻塞的api
有的,是pthread_mutex_trylock() 和 try_lock(),当获取不到锁时这两者并不阻塞当前线程,而是立即返回,当pthread_mutex_trylock()获取到锁时返回0,而std::mutex::try_lock()方法获取不到锁时返回false -
std::lock_guard 和 std::unique_lock区别
- 相同点:两者RAII技术(资源获取即初始化)
- 不同点:std::unique_lock灵活性高于std::lock_guard,前者可以在任何时间解锁和锁定,而std::lock_guard在构造时锁定,在析构时解锁,不能手动控制。所有权:std::unique_lock 支持所有权转移,而std::lock_guard不支持。性能:由于std::unique_lock灵活性较高,性能差一些
实现一个lock_guard
class lock_guard { explicit lock_guard(std::mutex &m):mutex_(m) {//当将一个参数传递给构造函数时,如果构造函数声明中使用了 explicit 关键字,则只能使用显式转换进行转换,而不能进行隐式转换。 mutex_.lock(); } ~lock_guard() { mutex_.unlock(); } private: std::mutex &mutex_; };
-
如何避免死锁:
- 避免循环等待,如果需要在业务中获取不同的锁,保证所有业务按照相同的顺序获取锁
- 打破互斥条件。将互斥访问的资源改为共享资源,并在中间建立一个队列缓冲区,每个进程依次访问。将独享锁改为共享锁(比如信号量为1的为独占锁,大于1就为共享锁)
- 破坏不剥夺,不抢占原则。请求新资源无法满足时,必须释放已有资源。(可能导致原有进程任务无法推进)
- 使用超时锁,当锁超时,自动释放锁
- 使用try_lock,当锁被占用时,返回false并继续执行
- 锁的粒度要适中,只保护竞态数据而不是整个过程。
- 资源剥夺
挂起死锁进程,剥夺其资源,将资源分配和其他进程 - 撤销进程
- 进程回退
回到足以避免死锁的地方,需要记录历史信息,设置还原点
-
mutex和spin_lock的区别
- mutex时sleep-waiting.也就是说没有获得mutex时,会有上下文切换,将自己加到忙等待队列中,知道另外一个线程释放mutex并唤醒它,而这时CPU是空闲的,可以调度别的任务处理。(事实上这种上下文切换对已经拿了锁的那个线程是有影响的,因为当该线程释放锁时,它需要通知操作系统去唤醒那些被阻塞的线程,这也是额外的开销)
- 而自旋锁spin_lock是busy-waiting,即没有可用的锁时,就一直忙等待并不停地进行锁请求
- 总结:Mutex适合对锁操作非常频繁的场景,有比较大的花销,但是在保证一定性能的前提下能提供更大的灵活度。
spin_lock性能更好,因为花费较少的CPU指令,但是它只适用于临界资源运行时间很短的场景。
- 什么是锁?有什么作用?
进程的状态转换图是什么?
参考进程状态转换
- 运行态(running):进程占有处理器正在运行
- 就绪态(ready):进程具备运行条件,等待系统分配处理器以便运行
- 等待态(wait):又称阻塞态或睡眠态,指进程不具备运行条件,正在等待某个事件发生而暂停运行
cpu能同时运行多个进程和多个线程吗?
任何给定时刻,CPU只能只能执行一条指令
- 单核CPU,单个MMU,只能同时运行一个进程里的一个线程
- 多核CPU,单个MMU,如双核,那这个CPU就能同时运行两个线程,但同一时刻只能运行一个进程
- 多个CPU,多个MMU,多个线程多个进程同时执行
进程间的资源竞争有哪些种类?
- 互斥竞争(Mutual Exclusion)
多个进程或线程试图同时访问一个只能被一个线程或进程使用的资源。 - 死锁竞争(Deadlock)
多个进程或线程因为同时等待对方释放资源而相互阻塞的状态 - 饥饿竞争(Starvation)
某进程或线程由于长时间无法获取所需资源而无法执行,或者即使获取了资源但是无法获得CPU。 - 优先级反转竞争(Priority Inversion)
低优先级进程或线程持有资源,中优先级已经在等待其资源,这时高优先级又来需要使用该资源 - 竞态条件竞争(Race Condition)
多个进程和线程试图同时访问共享资源,而资源的最终状态取决于进程的执行顺序,如果执行顺序不当,可能导致结果不符合预期。
解决以上资源竞争问题通常需要采用各种同步机制和算法,如互斥量,信号量等。
进程间资源竞争解决
-
信号量提供了进程间或线程间同步的机制。所谓同步,是指并发的实体之间相互制约,相互等待的一种机制,保证实体对共享资源的有条件访问。
信号量就是为了保护共享资源,让共享资源有序被访问的机制。 -
死锁的解除
- 逐个终止死锁进程,直到死锁接触
- 事先规定好进程被抢占的顺序以使代价最小。发生死锁时,选择一个作为牺牲品,抢占它的资源给其他进程,然后将这个牺牲品进程回滚到过去的某个安全状态。
- 银行家算法。它是一个避免死锁的著名算法,允许进程动态申请资源,但系统进行资源分配前,应先计算此次分配资源的安全性,如果此时分配不会导致进程进入不安全状态,则分配,否则等待。(资源的最大需求量)
系统如何保证多个进程并发执行的?
- 并行和并发
并行是指两个或多个事件在同一时刻发生,只会出现在多CPU多核场景下
并发是指两个或多个事件在同一时间间隔发生,宏观下似乎多个进程同时进行,微观下一次只能执行一个进程。 - 保证多进程的并发:
- 时间片轮转调度, 操作系统使用时间片轮转调度算法,将CPU的执行时间划分为小的时间片,并按一定的顺序将这些时间片分配给不同的进程。
- 上下文切换:当操作系统决定切换到下一个进程时,它会保存当前进程的执行状态(当前任务的上下文,也就是CPU寄存器和程序计数器),然后加载下一个进程的执行状态(加载新任务到这些寄存器和程序计数器).
- 同步和互斥机制:操作系统提供了各种同步和互斥机制,如信号量,互斥锁等,用于控制进程对共享资源的访问
操作系统如何处理中断和异常
- 中断(Interrupt)
执行程序时,突然发生的一些事件,例如输入输出操作完成,定时器超时,硬件错误等等。当发生中断时,操作系统会暂停当前进程的执行,切换到中断服务函数并处理中断事件,处理完毕后,操作系统回到原来的进程并继续执行。 - 异常(Exception)
程序执行中出现的错误或非正常情况(如:除以0,访问对象不存在等)。当发生异常时,操作系统会停止当前进程的执行,并处理异常。处理完毕后,操作系统会终止当前进程并回收资源。
进程同步与互斥以及信号量机制
信号量机制可以让用户通过操作系统提供的一对原语对信号量进行操作,从而方便地实现进程互斥和进程同步。
信号量 (semaphore)其实就是一个变量,它可以记录系统中某个资源的数量,而原语指signal和wait,可以看作是两个函数。
参考:信号量机制
注意:value是可用资源数,大于零是存在可用资源,小于零说明无可用资源并且有其他进程等待。等于零说明当前等待队列里没有阻塞的进程,但是仍然无可用资源。
操作系统中如何实现文件的共享和保护?
参考:文件的共享和保护
文件共享使多个用户(进程)共享同一份文件,系统中只需要保留该文件的一份副本。
- 文件的共享
- 硬链接
基于索引节点的共享方式。将共享文件或子目录的索引节点链接到用户的目录中,所谓索引节点,即将文件的物理地址及其他文件属性等存放到索引节点。
- 软连接
为使用户B能共享用户A的一个文件F,可以由系统创建一个Link类型新文件,也取名为F,并将新文件F写入用户B的目录中,新文件中只包含被链接文件的路径,这样的链接方法也称为符号链接
- 硬链接
- 文件保护文件保护通过口令保护,加密保护(前两者为了防止用户文件被他人存取或窃取)和访问控制(控制用户对文件的访问方式)实现
什么是进程调度?常用的调度算法有哪些?
- 先来先服务法(Linux)
- 短作业进程优先法
- 时间片轮转法(Linux)
- 多级反馈调度算法
- 多级反馈队列调度算法
什么是异步编程?它如何实现并发和并行?
- 使用同步编程方式,每个线程同时只能发起一个请求,并同步等待返回,所以为了提高性能,就需要引入更多的线程来实现并行化处理,但是多线程会引发对共享资源的争抢和并发问题,另外, 操作系统层面对线程的个数是有限制的。
- 异步编程是让程序并发运行的一种手段。它允许多个事件同时发生,当某个非核心业务程序需要长时间运行时,他不会阻塞当前执行主业务流程,主程序可以继续执行下面的操作。
核心思路:采用多线程优化性能,将串行操作变成并行操作。异步模式设计的程序设计可以显著减少减少线程等待,从而在高吞吐量场景中,极大提升系统的整体性能,显著降低时延。
消息队列- 消息队列天生就是异步架构,就有超高吞吐量和超低时延。
- 主要角色包括:消息生产者,消息消费者和消息队列
- 消息生产者就是主应用程序,生产者将调用请求封装成消息发送给消息队列。消息队列就是缓存消息,等待消费者消费。消息消费者,从消息队列中拉取,消费信息,完成业务逻辑处理。Kafka
什么是事件驱动编程(event-driven programming)?它与异步编程有何关系?
事件驱动是一种用于设计应用的软件架构和模型,程序的执行流由外部事件来决定,它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
- 事件队列(event queue):接收事件的入口,存储待处理事件
- 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元
- 事件通道(event channel):分发器与处理器之间的联系渠道
- 事件处理器(event processor):实现业务逻辑
reactor: 同步非阻塞IO,reactor非常重要的一环就是调用函数(read, receive, send)来完成数据拷贝,这部分是应用程序自己完成的,内核只负责通知监控的事件到来了。
把事件处理业务请求放到消息队列里,线程池的线程(消费者)依次从队列里取出事件,执行相应业务逻辑,结束后归还线程给线程池。由此reactor过程变成了异步操作。
什么是线程池?如何实现线程池的管理和调度?
参考文章:一文读懂线程池的实现原理
c++11实现一个线程池,多任务的分配与调度
创建线程
- 什么是线程池:
线程池是一种线程使用的模式,线程过多或者频繁地创建线程和销毁线程会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着被分配可并发执行的任务,这避免了在处理短时间任务时创建与销毁线程的代价,以及保证了线程的可复用性。 - 原理:
- 预先创建预定数量的线程
- 将多个任务加入到任务队列,类似于生产者消费者,多个线程相当于消费者,提供任务的成为生产者。
- 当任务队列有任务时,线程就会争抢这些任务,但每次只允许一个线程能够得到任务。
- 任务执行完成后,线程被释放并被归还到到线程池。