分享面试总结,涉及C++、算法、数据结构、操作系统、计算机网络、Linux、数据库、设计模式 等,后面持续更新~
内容多为收集、整理总结,视频、书籍学习所得,如有错误请指出,万分感谢!!!
※代表高频问题(参考)
操作系统篇— √1
1. 操作系统特点?
并发性、共享性、虚拟性、不确定性,每个特点举例说明。
后面很多会涉及到Linux系统
2. ※※进程?
- 进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程;
- 进程可以认为是程序执行的一个实例,进程是系统进行资源分配的最小单位,且每个进程拥有独立的地址空间;
- 一个进程无法直接访问另一个进程的变量和数据结构,如果希望一个进程去访问另一个进程的资源,需要使用进程间的通信,比如:管道、消息队列等
- 线程是进程的一个实体,是进程的一条执行路径;比进程更小的独立运行的基本单位,线程也被称为轻量级进程,一个程序至少有一个进程,一个进程至少有一个线程;
- 进程状态:有运行、阻塞、就绪三个基本状态;
进程调度算法:先来先服务调度算法、短作业优先调度算法、非抢占式优先级调度算法、抢占式优先级调度算法、高响应比优先调度算法、时间片轮转法调度算法。
网上有很多解释很好的,这里抛砖引玉。
※※线程?
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
3. ※※※进程与线程的区别?
- 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
- 同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的;
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程崩溃,所以多进程比多线程健壮;
- 进程切换,消耗的资源大。所以涉及到频繁的切换,使用线程要好于进程;
- 两者均可并发执行;
- 每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
扩展,请查阅网上资料。
4. 进程状态?
- 新状态:进程已经创建
- 就绪态:进程做好了准备,准备执行,等待分配处理机
- 执行态:该进程正在执行;
- 阻塞态:等待某事件发生才能执行,如等待I/O完成;
- 终止状态。
5. 进程的创建过程?需要哪些函数?需要哪些数据结构?
- fork函数创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容;
- vfork创建的子进程与父进程共享数据段,而且由vfork创建的子进程将先于父进程运行;
- linux上创建线程一般使用的是pthread库,实际上linux也给我们提供了创建线程的系统调用,就是clone。
6. 进程创建子进程,fork详解?
- 函数原型pid_t fork(void);//void代表没有任何形式参数
- 除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进程,即调用fork函数的进程为父进程,新建的进程为子进程。
- fork函数不需要任何参数,对于返回值有三种情况:
① 对于父进程,fork函数返回新建子进程的pid;
② 对于子进程,fork函数返回0;
③ 如果出错,fork函数返回-1。
int pid=fork();
if(pid < 0){
//失败,一般是该用户的进程数达到限制或者内存被用光了
…
}
else if(pid == 0){
//子进程执行的代码
…
}
else{
//父进程执行的代码
…
}
7. ※子进程和父进程怎么通信?
- 在Linux系统中实现父子进程的通信可以采用pipe()和fork()函数进行实现;
- 对于父子进程,在程序运行时首先进入的是父进程,其次是子进程,在此我个人认为,在创建父子进程的时候程序是先运行创建的程序,其次在复制父进程创建子进程。fork()函数主要是以父进程为蓝本复制一个进程,其ID号和父进程的ID号不同。对于结果fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号。
- 管道:是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称pipe文件。
- 写进程在管道的尾端写入数据,读进程在管道的首端读出数据。
8. ※计一个线程池,内存池?
-
为什么需要线程池?
大多数的网络服务器,包括Web服务器都具有一个特点,就是单位时间内必须处理数目巨大的连接请求,但是处理时间却是比较短的。在传统的多线程服务器模型中是这样实现的:一旦有个请求到达,就创建一个新的线程,由该线程执行任务,任务执行完毕之后,线程就退出。这就是”即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数非常频繁,那么服务器就将处于一个不停的创建线程和销毁线程的状态。这笔开销是不可忽略的,尤其是线程执行的时间非常非常短的情况。 -
线程池原理:
在应用程序启动之后,就马上创建一定数量的线程,放入空闲的队列中。这些线程都是处于阻塞状态,这些线程只占一点内存,不占用CPU。当任务到来后,线程池将选择一个空闲的线程,将任务传入此线程中运行。当所有的线程都处在处理任务的时候,线程池将自动创建一定的数量的新线程,用于处理更多的任务。执行任务完成之后线程并不退出,而是继续在线程池中等待下一次任务。当大部分线程处于阻塞状态时,线程池将自动销毁一部分的线程,回收系统资源。 -
线程池的作用:
需要大量的线程来完成任务,且完成任务的时间比较短;对性能要求苛刻的应用;对性能要求苛刻的应用。 -
内存池的原理:
在软件开发中,有些对象使用非常频繁,那么我们可以预先在堆中实例化一些对象,我们把维护这些对象的结构叫“内存池”。在需要用的时候,直接从内存池中拿,而不用从新实例化,在要销毁的时候,不是直接free/delete,而是返还给内存池。把那些常用的对象存在内存池中,就不用频繁的分配/回收内存,可以相对减少内存碎片,更重要的是实例化这样的对象更快,回收也更快。当内存池中的对象不够用的时候就扩容。 -
内存池的优缺点:
内存池对象不是线程安全的,在多线程编程中,创建一个对象时必须加锁。
扩展:自己做过实现吗?
9. ※进程和作业的区别?
- 进程是程序的一次动态执行,属于动态概念;
- 一个进程可以执行一个或几个程序,同一个程序可由几个进程执行;
- 程序可以作为一种软件资源长期保留,而进程是程序的一次执行;
- 进程具有并发性,能与其他进程并发执行;
- 进程是一个独立的运行单位。
10. ※※死锁是什么?必要条件?如何解决?
所谓死锁,是指多个进程循环等待它方占有的资源而无限期地僵持下去的局面。很显然,如果没有外力的作用,那麽死锁涉及到的各个进程都将永远处于封锁状态。当两个或两个以上的进程同时对多个互斥资源提出使用要求时,有可能导致死锁。
〈1〉 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。
〈2〉 不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。
〈3〉 占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。
〈4〉 循环等待条件。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,…,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。
死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
<1>打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
<2>打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
<3>打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。
<4>打破循环等待条件。实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。
死锁避免:银行家算法
11. 鸵鸟策略?
假设的前提是,这样的问题出现的概率很低。比如,在操作系统中,为应对死锁问题,可以采用这样的一种办法。当系统发生死锁时不会对用户造成多大影响,或系统很少发生死锁的场合采用允许死锁发生的鸵鸟算法,这样一来可能开销比不允许发生死锁及检测和解除死锁的小。如果死锁很长时间才发生一次,而系统每周都会因硬件故障、编译器错误或操作系统错误而崩溃一次,那么大多数工程师不会以性能损失或者易用性损失的代价来设计较为复杂的死锁解决策略,来消除死锁。
鸵鸟策略的实质:出现死锁的概率很小,并且出现之后处理死锁会花费很大的代价,还不如不做处理,OS中这种置之不理的策略称之为鸵鸟策略(也叫鸵鸟算法)。
12. ※银行家算法?
在避免死锁的方法中,所施加的限制条件较弱,有可能获得令人满意的系统性能。在该方法中把系统的状态分为安全状态和不安全状态,只要能使系统始终都处于安全状态,便可以避免发生死锁。
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。
扩展:如何实现,请网上自行查阅!
13. ※※※进程间通信方式有几种,他们之间的区别?
- 管道
管道,通常指无名管道。
① 半双工的,具有固定的读端和写端;
② 只能用于具有亲属关系的进程之间的通信;
③ 可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数。但是它不是普通的文件,并不属于其他任何文件系统,只能用于内存中。
④ int pipe(int fd[2]);当一个管道建立时,会创建两个文件文件描述符,要关闭管道只需将这两个文件描述符关闭即可。 - FIFO(有名管道)
① FIFO可以再无关的进程之间交换数据,与无名管道不同;
② FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中;
③ int mkfifo(const char* pathname,mode_t mode); - 消息队列
① 消息队列,是消息的连接表,存放在内核中。一个消息队列由一个标识符来标识;
② 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
③ 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
④ 消息队列可以实现消息的随机查询 - 信号量
① 信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
② 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
③ 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作; - 共享内存
① 共享内存,指两个或多个进程共享一个给定的存储区;
② 共享内存是最快的一种进程通信方式,因为进程是直接对内存进行存取;
③ 因为多个进程可以同时操作,所以需要进行同步;
④ 信号量+共享内存通常结合在一起使用。
其实还有几种,如套接字等,请自行查阅。
14. ※※※线程同步的方式?
线程同步是指多线程通过特定的设置来控制线程之间的执行顺序,也可以说在线程之间通过同步建立起执行顺序的关系。
主要四种方式,临界区、互斥对象、信号量、事件对象;其中临界区和互斥对象主要用于互斥控制,信号量和事件对象主要用于同步控制。
-
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快、适合控制数据访问。在任意一个时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
-
互斥对象:互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。
-
信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。
-
事件对象:通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
15. ※操作系统调度策略?
1.先来先服务调度算法:先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
2.短作业(进程)优先调度算法:短作业(进程)优先调度算法SJ§F,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
3.高优先权优先调度算法:为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。
3.1)非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
3.2)抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i时,就将其优先权Pi与正在执行的进程j的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
4.高响应比优先调度算法:在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为
在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
5.时间片轮转法:在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。
6.多级反馈队列调度算法:前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法
16. 各个操作系统进程调度策略?
参考:https://www.cnblogs.com/wanghuaijun/p/8046992.html
Unix系统是多用户,多任务的操作系统,其分时性是通过对用户进程频繁的调度来实现的,系统的调度程序分成两部分,即处理机调度程序(swtch)和进程对换程序(sched)。传统Unix操作系统的调度算法必须实现几个互相冲突的目标:进程响应时间尽可能快,后台作业的吞吐量尽可能高,进程的饥饿现象尽可能避免,低优先级和高优先级进程的需要尽可能调和等等。决定什么时候以怎样的方式选择一个新进程运行的这组规则就是所谓的调度策略(scheduling policy)。
Linux的进程调度是基于分时技术(time-sharing)。允许多个进程“并发”运行就意味着CPU的时间被粗略地分成“片”,给每个可运行进程分配一片。当然,单处理器在任何给定的时刻只能运行一个进程。当一个并发执行的进程其时间片或时限(quantum)到期时还没有终止,进程切换就可以发生。分时依赖于定时中断,因此,对进程是透明的。为保证CPU分时,不需要在程序中插入额外的代码。
Windows实现了一个优先驱动的,抢先式的调度系统——具有最高优先级的可运行线程总是运行,而该线程可能仅限于在允许它运行的处理器上运行,这种现象称为处理器亲和性,在默认的情况下,线程可以在任何一个空闲的处理器上运行,但是,你可以使用windows调度函数,或者在映像头部设置一个亲和性掩码来改变处理器亲和性。
17. ※页和段的区别?
- 页是信息的物理单位,分页是由于系统管理的需要。段是信息的逻辑单位,分段是为了满足用户的要求。
- 页的大小固定且由系统决定,段的长度不固定,决定于用户所编写的程序,通常由编译程序在对源程序紧进行编译时,根据信息的性质来划分。
- 分页的作业的地址空间是一维的,程序员只需要利用一个记忆符,即可表示一个地址。分段的作业地址空间则是二维的,程序员在标识一个地址时,既需要给出段名,又需要给出段的地址值。
18. ※孤儿进程和僵尸进程的区别?怎么避免这两类进程?守护进程?
1、 一般情况下,子进程是由父进程创建,而子进程和父进程的退出是无顺序的,两者之间都不知道谁先退出。正常情况下父进程先结束会调用wait或者waitpid函数等待子进程完成再退出,而一旦父进程不等待直接退出,则剩下的子进程会被init(pid=1)进程接收,成会孤儿进程。(进程树中除了init都会有父进程)
2、 如果子进程先退出了,父进程还未结束并且没有调用wait或者waitpid函数获取子进程的状态信息,则子进程残留的状态信息(task_struct结构和少量资源信息)会变成僵尸进程。
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
3、 守护进程(daemon)是指在后台运行,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。
19. ※守护进程是什么?怎么实现?
-
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,是一种很有用的进程。
-
守护进程特点:
a) 守护进程最重要的特性是后台运行。
b) 守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
c) 守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(shell)执行。
实现:
- 在父进程中执行fork并exit推出;
- 在子进程中调用setsid函数创建新的会话;
- 在子进程中调用chdir函数,让根目录”/”成为子进程的工作目录;
- 在子进程中调用umask函数,设置进程的umask为0;
- 在子进程中关闭任何不需要的文件描述符。
- 进程、线程共享的资源是什么?
线程私有:线程栈,寄存器,程序寄存器
共享:堆,地址空间,全局变量,静态变量
进程私有:地址空间,堆,全局变量,栈,寄存器
共享:代码段,公共数据,进程目录,进程ID
未完待续~