操作系统面试题集锦

      1. 操作系统特点

并发性、共享性、虚拟性、不确定性。

      1. 什么是进程
  1. 进程是指在系统中正在运行的一个应用程序程序一旦运行就是进程;
  2. 进程可以认为是程序执行的一个实例,进程是系统进行资源分配的最小单位,且每个进程拥有独立的地址空间
  3. 一个进程无法直接访问另一个进程的变量和数据结构,如果希望一个进程去访问另一个进程的资源,需要使用进程间的通信,比如:管道、消息队列等
  4. 线程是进程的一个实体,是进程的一条执行路径;比进程更小的独立运行的基本单位,线程也被称为轻量级进程,一个程序至少有一个进程,一个进程至少有一个线程;
      1. 进程

进程是程序的一次执行,该程序可以与其他程序并发执行;

进程有运行、阻塞、就绪三个基本状态;

进程调度算法:先来先服务调度算法、短作业优先调度算法、非抢占式优先级调度算法、抢占式优先级调度算法、高响应比优先调度算法、时间片轮转法调度算法;

      1. 进程与线程的区别
  1. 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
  2. 同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的;
  3. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程崩溃,所以多进程比多线程健壮
  4. 进程切换,消耗的资源大。所以涉及到频繁的切换,使用线程要好于进程;
  5. 两者均可并发执行;
  6. 每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
      1. 进程状态转换图

  1. 新状态:进程已经创建
  2. 就绪态:进程做好了准备,准备执行,等待分配处理机
  3. 执行态:该进程正在执行;
  4. 阻塞态:等待某事件发生才能执行,如等待I/O完成;
  5. 终止状态
      1. 进程的创建过程?需要哪些函数?需要哪些数据结构?
  1. fork函数创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容;
  2. vfork创建的子进程与父进程共享数据段,而且由vfork创建的子进程将先于父进程运行;
  3. linux上创建线程一般使用的是pthread库,实际上linux也给我们提供了创建线程的系统调用,就是clone;
      1. 进程创建子进程,fork详解
  1. 函数原型

pid_t fork(void); //void代表没有任何形式参数

  1. 除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进程,即调用fork函数的进程为父进程,新建的进程为子进程。
  2. fork函数不需要任何参数,对于返回值有三种情况:
  • 对于父进程fork函数返回新建子进程的pid
  • 对于子进程fork函数返回 0
  • 如果出错, fork 函数返回 -1

int pid=fork();
if(pid < 0){
//失败,一般是该用户的进程数达到限制或者内存被用光了   
........     
}
else if(pid == 0){
//子进程执行的代码
......
}
else{
//父进程执行的代码
.........
}

      1. 子进程和父进程怎么通信?
  1. 在Linux系统中实现父子进程的通信可以采用pipe()和fork()函数进行实现;
  2. 对于父子进程,在程序运行时首先进入的是父进程,其次是子进程,在此我个人认为,在创建父子进程的时候程序是先运行创建的程序,其次在复制父进程创建子进程。fork()函数主要是以父进程为蓝本复制一个进程,其ID号和父进程的ID号不同。对于结果fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号。
  3. 管道:是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称pipe文件。
  4. 写进程在管道的尾端写入数据,读进程在管道的首端读出数据。
      1. 进程和作业的区别?
  1. 进程是程序的一次动态执行,属于动态概念;
  2. 一个进程可以执行一个或几个程序,同一个程序可由几个进程执行
  3. 程序可以作为一种软件资源长期保留,而进程是程序的一次执行
  4. 进程具有并发性,能与其他进程并发执行;
  5. 进程是一个独立的运行单位
      1. 死锁是什么?必要条件?如何解决?

所谓死锁,是指多个进程循环等待它方占有的资源而无限期地僵持下去的局面。很显然,如果没有外力的作用,那麽死锁涉及到的各个进程都将永远处于封锁状态。当两个或两个以上的进程同时对多个互斥资源提出使用要求时,有可能导致死锁。

  1. 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。
  2. 不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。
  3. 占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。
  4. 循环等待条件。存在一个进程等待序列{P1,P2,...,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,......,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。

死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

<1>打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

<2>打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。    

<3>打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。

<4>打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁

死锁避免:银行家算法

      1. 鸵鸟策略

假设的前提是,这样的问题出现的概率很低。比如,在操作系统中,为应对死锁问题,可以采用这样的一种办法。当系统发生死锁不会对用户造成多大影响,或系统很少发生死锁的场合采用允许死锁发生的鸵鸟算法,这样一来可能开销比不允许发生死锁及检测和解除死锁的小。如果死锁很长时间才发生一次,而系统每周都会因硬件故障、编译器错误或操作系统错误而崩溃一次,那么大多数工程师不会以性能损失或者易用性损失的代价来设计较为复杂的死锁解决策略,来消除死锁。鸵鸟策略的实质:出现死锁的概率很小,并且出现之后处理死锁会花费很大的代价,还不如不做处理,OS中这种置之不理的策略称之为鸵鸟策略(也叫鸵鸟算法)。

      1. 银行家算法

在避免死锁的方法中,所施加的限制条件较弱,有可能获得令人满意的系统性能。在该方法中把系统的状态分为安全状态和不安全状态,只要能使系统始终都处于安全状态,便可以避免发生死锁

银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。

设进程cusneed提出请求REQUEST [i],则银行家算法按如下规则进行判断。

(1)如果REQUEST [cusneed] [i]<= NEED[cusneed][i],则转(2);否则,出错。

(2)如果REQUEST [cusneed] [i]<= AVAILABLE[i],则转(3);否则,等待。

(3)系统试探分配资源,修改相关数据:

AVAILABLE[i]-=REQUEST[cusneed][i];

ALLOCATION[cusneed][i]+=REQUEST[cusneed][i];

NEED[cusneed][i]-=REQUEST[cusneed][i];

(4)系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。

      1. 进程间通信方式有几种,他们之间的区别是什么?
  1. 管道

管道,通常指无名管道。

  • 半双工的,具有固定的读端和写端;
  • 只能用于具有亲属关系的进程之间的通信;
  • 可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数。但是它不是普通的文件,并不属于其他任何文件系统,只能用于内存中。
  • Int pipe(int fd[2]);当一个管道建立时,会创建两个文件文件描述符,要关闭管道只需将这两个文件描述符关闭即可。
  1. FiFO(有名管道)
  • FIFO可以再无关的进程之间交换数据,与无名管道不同;
  • FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中;
  • Int mkfifo(const char* pathname,mode_t mode);
  1. 消息队列
  • 消息队列,是消息的连接表,存放在内核中。一个消息队列由一个标识符来标识
  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
  • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
  • 消息队列可以实现消息的随机查询
  1. 信号量
  • 信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
  • 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作;
  1. 共享内存
  • 共享内存,指两个或多个进程共享一个给定的存储区
  • 共享内存是最快的一种进程通信方式,因为进程是直接对内存进行存取
  • 因为多个进程可以同时操作,所以需要进行同步
  • 信号量+共享内存通常结合在一起使用。
      1. 线程同步的方式?怎么用?
  1. 线程同步是指多线程通过特定的设置来控制线程之间的执行顺序,也可以说在线程之间通过同步建立起执行顺序的关系;
  2. 主要四种方式,临界区、互斥对象、信号量、事件对象;其中临界区和互斥对象主要用于互斥控制,信号量和事件对象主要用于同步控制;
  3. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快、适合控制数据访问。在任意一个时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
  4. 互斥对象:互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。
  5. 信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。但是当前可用计数减小 到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离 开的同时通过ReleaseSemaphore ()函数将当前可用资源计数加1 。在任何时候当前可用资源计数决不可能大于最大资源计数。
  6. 事件对象:通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
      1. 页和段的区别?
  1. 页是信息的物理单位,分页是由于系统管理的需要。段是信息的逻辑单位,分段是为了满足用户的要求
  2. 页的大小固定且由系统决定,段的长度不固定,决定于用户所编写的程序,通常由编译程序在对源程序紧进行编译时,根据信息的性质来划分。
  3. 分页的作业的地址空间是一维的,程序员只需要利用一个记忆符,即可表示一个地址。分段的作业地址空间则是二维的,程序员在标识一个地址时,既需要给出段名,又需要给出段的地址值
      1. 孤儿进程和僵尸进程的区别?怎么避免这两类进程?守护进程?
  1. 一般情况下,子进程是由父进程创建,而子进程和父进程的退出是无顺序的,两者之间都不知道谁先退出。正常情况下父进程先结束会调用 wait 或者 waitpid 函数等待子进程完成再退出,而一旦父进程不等待直接退出,则剩下的子进程会被init(pid=1)进程接收,成会孤儿进程。(进程树中除了init都会有父进程)。
  2. 如果子进程先退出了,父进程还未结束并且没有调用 wait 或者 waitpid 函数获取子进程的状态信息,则子进程残留的状态信息( task_struct 结构和少量资源信息)会变成僵尸进程。

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数调用wait进行处理僵尸进程

原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

  1. 守护进程( daemon) 是指在后台运行没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务 。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断
      1. 守护进程是什么?怎么实现?
  1. 守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。
  2. 守护进程特点
  1. 守护进程最重要的特性是后台运行
  2. 守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。 
  3. 守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(shell)执行。
  1. 实现
  1. 在父进程中执行fork并exit推出;
  2. 在子进程中调用setsid函数创建新的会话;
  3. 在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
  4. 在子进程中调用umask函数,设置进程的umask为0;
  5. 在子进程中关闭任何不需要的文件描述符
      1. 线程和进程的区别?线程共享的资源是什么?
  1. 一个程序至少有一个进程,一个进程至少有一个线程
  2. 线程的划分尺度小于进程,使得多线程程序的并发性高
  3. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  4. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
  5. 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配
  6. 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 

线程私有:线程栈,寄存器,程序寄存器

共享:堆,地址空间,全局变量,静态变量

进程私有:地址空间,堆,全局变量,栈,寄存器

共享:代码段,公共数据,进程目录,进程ID

      1. 线程比进程具有哪些优势?
  1. 线程在程序中是独立的,并发的执行流,但是,进程中的线程之间的隔离程度要小
  2. 线程比进程更具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程共享同一个进程虚拟空间;
  3. 当操作系统创建一个进程时,必须为进程分配独立的内存空间,并分配大量相关资源;
      1. 什么时候用多进程?什么时候用多线程?
  1. 需要频繁创建销毁的优先用线程;
  2. 需要进行大量计算的优先使用线程;
  3. 强相关的处理用线程,弱相关的处理用进程;
  4. 可能要扩展到多机分布的用进程,多核分布的用线程;
      1. 协程是什么?
  1. 是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程;协程不是被操作系统内核管理,而完全是由程序所控制
  2. 协程的开销远远小于线程;
  3. 协程拥有自己寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈。
  4. 每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源。
  5. 跨平台、跨体系架构、无需线程上下文切换的开销、方便切换控制流,简化编程模型;
  6. 协程又称为微线程,协程的完成主要靠yeild关键字,协程执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行;
  7. 协程极高的执行效率,和多线程相比,线程数量越多,协程的性能优势就越明显;
  8. 不需要多线程的锁机制;
      1. 递归锁?
  1. 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
  2. 读写锁从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。
  3. Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。可递归锁也可称为可重入锁(reentrant mutex),非递归锁又叫不可重入锁(non-reentrant mutex)。二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。
      1. 用户态到内核态的转化原理?
  1. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

  1. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

  1. 外围设备的中断

外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

      1. 中断的实现与作用,中断的实现过程?
    1. 关中断,进入不可再次响应中断的状态,由硬件实现。
    2. 保存断点,为了在中断处理结束后能正确返回到中断点。由硬件实现。
    3. 中断服务程序入口地址送PC,转向中断服务程序。可由硬件实现,也可由软件实现。
    4. 保护现场、置屏蔽字、开中断,即保护CPU中某些寄存器的内容、设置中断处理次序、允许更高级的中断请求得到响应,实现中断嵌套。由软件实现。
    5. 设备服务,实际上有效的中断处理工作是在此程序段中实现的。由软件程序实现
    6. 退出中断。在退出时,又应进入不可中断状态,即关中断、恢复屏蔽字、恢复现场、开中断、中断返回。由软件实现。
      1. 系统中断是什么,用户态和内核态的区别
  1. 内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。 
  2. 这两种状态的主要差别是: 处于用户态执行时,进程所能访问的内存空间对象受到限制,其所处于占有的处理机是可被抢占的 ; 而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。 
      1. CPU中断
  1. CPU中断是什么
  • 计算机处于执行期间
  • 系统内发生了非寻常或非预期的急需处理事件
  • CPU暂时中断当前正在执行的程序而转去执行相应的事件处理程序;
  • 处理完毕后返回原来被中断处继续执行;
  1. CPU中断的作用
  • 可以使CPU和外设同时工作,使系统可以及时地响应外部事件;
  • 可以允许多个外设同时工作,大大提高了CPU的利用率;
  • 可以使CPU及时处理各种软硬件故障。
      1. 执行一个系统调用时,OS发生的过程,越详细越好
        1.执行用户程序(如:fork) 

2. 根据glibc中的函数实现,取得系统调用号并执行int $0x80产生中断。
3. 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进行内核模式)
4. 进行中断处理,根据系统调用表调用内核函数。
5. 执行内核函数。
6. 执行RESTORE_ALL并返回用户模式

      1. 函数调用和系统调用的区别?
  1. 系统调用
  • 操作系统提供给用户程序调用的一组特殊的接口。用户程序可以通过这组特殊接口来获得操作系统内核提供的服务;
  • 系统调用可以用来控制硬件;设置系统状态或读取内核数据;进程管理,系统调用接口用来保证系统中进程能以多任务在虚拟环境下运行;
  • Linux中实现系统调用利用了0x86体系结构中的软件中断
  1. 函数调用
  • 函数调用运行在用户空间
  • 它主要是通过压栈操作来进行函数调用
  1. 区别

      1. 经典同步问题解法:生产者与消费者问题,哲学家进餐问题,读者写者问题。
      2. 虚拟内存?使用虚拟内存的优点?什么是虚拟地址空间?
  1. 虚拟内存,虚拟内存是一种内存管理技术,它会使程序自己认为自己拥有一块很大且连续的内存,然而,这个程序在内存中不是连续的,并且有些还会在磁盘上,在需要时进行数据交换;
  2. 优点:可以弥补物理内存大小的不足;一定程度的提高反应速度;减少对物理内存的读取从而保护内存延长内存使用寿命;
  3. 缺点:占用一定的物理硬盘空间;加大了对硬盘的读写;设置不得当会影响整机稳定性与速度。
  4. 虚拟地址空间是对于一个单一进程的概念,这个进程看到的将是地址从0000开始的整个内存空间。虚拟存储器是一个抽象概念,它为每一个进程提供了一个假象,好像每一个进程都在独占的使用主存。每个进程看到的存储器都是一致的,称为虚拟地址空间。从最低的地址看起:程序代码和数据,堆,共享库,栈,内核虚拟存储器。大多数计算机的字长都是32位,这就限制了虚拟地址空间为4GB。
      1. 线程安全?如何实现?
  1. 如果你的代码所在的进程有多个线程同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  2. 线程安全问题都是由全局变量静态变量引起的。
  3. 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
  4. 对于线程不安全的对象我们可以通过如下方法来实现线程安全:
  • 加锁 利用Synchronized或者ReenTrantLock来对不安全对象进行加锁,来实现线程执行的串行化,从而保证多线程同时操作对象的安全性,一个是语法层面的互斥锁,一个是API层面的互斥锁.
  • 非阻塞同步来实现线程安全。原理就是:通俗点讲,就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生冲突,那就再采取其他措施(最常见的措施就是不断地重试,知道成功为止)。这种方法需要硬件的支持,因为我们需要操作和冲突检测这两个步骤具备原子性。通常这种指令包括CAS SC,FAI TAS等。
  • 线程本地化,一种无同步的方案,就是利用Threadlocal来为每一个线程创造一个共享变量的副本来(副本之间是无关的)避免几个线程同时操作一个对象时发生线程安全问题。
      1. linux文件系统
  1. 层次分析
  1. 用户层,日常使用的各种程序,需要的接口主要是文件的创建、删除、读、写、关闭等;
  2. VFS层,文件相关的操作都有对应的System Call函数接口,接口调用VFS对应的函数;
  3. 文件系统层,用户的操作通过VFS转到各种文件系统。文件系统把文件读写命令转化为对磁盘LBA的操作,起了一个翻译和磁盘管理的工作;
  4. 缓存层;
  5. 块设备层,块设备接口Block Device是用来访问磁盘LBA的层级,读写命令组合之后插入到命令队列,磁盘的驱动从队列读命令执行
  6. 磁盘驱动层;
  7. 磁盘物理层;
  1. 读取文件过程
  1. 据文件所在目录的inode信息,找到目录文件对应数据块
  2. 据文件名从数据块中找到对应的inode节点信息
  3. 从文件inode节点信息中找到文件内容所在数据块块号
  4. 读取数据块内容
      1. 常见的IO模型,五种?异步IO应用场景?有什么缺点?
  1. 同步

就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回也就是必须一件一件事做,等前一件做完了才能做下一件事。就是我调用一个功能,该功能没有结束前,我死等结果。

  1. 异步

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)

  1. 阻塞

阻塞调用是指调用结果返回之前当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。

  1. 非阻塞

指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。就是调用我(函数),我(函数)立即返回,通过select通知调用者

  1. 阻塞I/O

应用程序调用一个IO函数,导致应用程序阻塞等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。

  1. 非阻塞I/O

我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

  1. I/O复用

 I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这三个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

  1. 信号驱动I/O

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

  1. 异步I/O

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

      1. IO复用的原理?零拷贝?三个函数?epoll 的 LT 和 ET 模式的理解
  1. IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了。Linux中,提供了select、poll、epoll三种接口函数来实现IO复用。
  2. Select

select的缺点:

  • 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024。由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
  • 内核/用户空间内存拷贝问题,select需要大量句柄数据结构,产生巨大开销;
  • Select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生事件;
  • Select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么每次select调用还会将这些文件描述符通知进程。
  1. Poll

与select相比,poll使用链表保存文件描述符,一你才没有了监视文件数量的限制,但其他三个缺点依然存在

  1. Epoll

上面所说的select缺点在epoll上不复存在,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。Epoll是事件触发的,不是轮询查询的。没有最大的并发连接限制,内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递。

 

区别总结:

  1. 支持一个进程所能打开的最大连接数
  • Select最大1024个连接,最大连接数有FD_SETSIZE宏定义,其大小是32位整数表示,可以改变宏定义进行修改,可以重新编译内核,性能可能会影响;
  • Poll没有最大连接限制,原因是它是基于链表来存储的
  • 连接数限数有上限,但是很大;
  1. FD剧增后带来的IO效率问题
  • 因为每次进行线性遍历,所以随着FD的增加会造成遍历速度下降,效率降低;
  • Poll同上;
  • 因为epool内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的现象下降的性能问题。
  1. 消息传递方式
  • Select内核需要将消息传递用户空间,都需要内核拷贝
  • Poll同上;
  • Epoll通过内核用户空间共享来实现的。

 

epoll 的 LT 和 ET 模式的理解

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger),LT是默认模式。

区别:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

      1. Linux是如何避免内存碎片的
  1. 固定式分区分配中, 为将一个用户作业装入内存, 内存分配程序从系统分区表中找出一个能满足作业要求的空闲分区分配给作业, 由于一个作业的大小并不一定与分区大小相等, 因此, 分区中有一部分存储空间浪费掉了. 由此可知, 固定式分区分配中存在内碎片.
  2. 可变式分区分配中, 为把一个作业装入内存, 应按照一定的分配算法从系统中找出一个能满足作业需求的空闲分区分配给作业, 如果这个空闲分区的容量比作业申请的空间容量要大, 则将该分区一分为二, 一部分分配给作业, 剩下的部分仍然留作系统的空闲分区。由此可知,可变式分区分配中存在外碎片.
  3. 伙伴系统
  4. 据可移动性组织页避免内存碎片
      1. 递归的原理是啥?递归中遇到栈溢出怎么解决
  1. 基本原理

第一:每一级的函数调用都有它自己的变量。 
第二:每一次函数调用都会有一次返回,并且是某一级递归返回到调用它的那一级,而不是直接返回到main()函数中的初始调用部分。 
第三:递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。例如在上面的程序中,打印语句#1位于递归调用语句之前,它按照递归调用的顺序被执行了4次,即依次为第一级、第二级、第三级、第四级。 
第四:递归函数中,位于递归调用后的语句的执行顺序和各个被调函数的顺序相反。例如上面程序中,打印语句#2位于递归调用语句之后,其执行顺序依次是:第四级、第三级、第二级、第一级。(递归调用的这种特性在解决涉及到反向顺序的编程问题中很有用,下文会说到) 
第五:虽然每一级递归都有自己的变量,但是函数代码不会复制。 
第六:递归函数中必须包含终止递归的语句。通常递归函数会使用一个if条件语句或其他类似语句一边当函数参数达到某个特定值时结束递归调用,如上面程序的if(n > 4)。

  1. 用递归实现算法时,有两个因素是至关重要的:递归式递归边界
  2. 函数调用时通过栈(Stack)来实现的,每当调用一个函数,栈就会加一层栈帧,函数返回就减一层栈帧。而栈资源有限,当递归深度达到一定程度后,就会出现意想不到的结果,比如堆栈溢出;
  3. 利用循环函数或者栈加while循环来代替递归函数。
      1. ++i是否是原子操作

i++的操作分三步:

(1)栈中取出i

(2)i自增1

(3)将i存到栈

所以i++不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致i的值不正确自增

二.++i

在多核的机器上,cpu在读取内存i时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。

综上,我认为i++和++i都不是原子操作。

      1. 缺页中断,页表寻址
  1. 一个进程对应一个页表,分页存储机制,一个进程对应很多页,执行进程时并不是所有页装入内存中,部分装入内存,当需要的那页不存在内存中,将发生缺页中断,将需要的那页从外存中调入内存中;
  2. 页表寻址,页分为页号(从0开始编号)与页内偏移地址,两个寄存器,页表基地址寄存器,页表长度寄存器,块表;页的大小相同,内存中的块与页大小相同,页大小相同,页在逻辑上连续在物理上不连续;
  3. 调页算法:先进先出,最佳页面置换算法(OPT),最近最久未使用(NRU),最近最少使用置换算法(LRU),先进先出算法(FIFO)会导致Baley问题;抖动,页面在内存与外存中的频繁调页;
  4. 程序局部性原理,时间局部性、空间局部性;
      1. LRU的实现
  1. 用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据的时间戳自增,并将新数据时间戳置为0插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项时间戳置为0。当数组空间已经满时,将时间戳最大的数据项淘汰;
  2. 利用一个链表来实现,每次新插入数据的时候将新数据插入到链表头部;每次缓存命中,则将数据移动到链表头部;那么当链表满时,就将链表尾部的数据丢弃;
  3. 利用链表和hashmap。当需要插入新的数据项 的时候,如果新数据命中,则把该节点放到链表头部,如果不存在,则将新数据放在链表头部。若缓存满了,则将链表尾部的节点删除。
      1. 内存分区
  1. 固态分区,分区大小固定,但并不一定相同;
  2. 可变分区,分区大小动态变化,首先适配、最佳适配、最差适配、下一次适配;
      1. 伙伴系统相关
  1. 伙伴系统是一种经典的内存管理方法。Linux伙伴系统的引入为内核提供了一种用于分配一组连续的页而建立的一种高效的分配策略,并有效的解决了外碎片问题
  2. Linux中的内存管理的“页”大小为4KB把所有的空闲页分组为11个块链表,每个块链表分别包含大小为1248163264128256512和1024个连续页框的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该块大小的整数倍。
  3. 当向内核请求分配(2^(i-1)2^i]数目的页块时,按照2^i页块请求处理。如果对应的块链表中没有空闲页块,则在更大的页块链表中找。当分配的页块中有多余的页时,伙伴系统根据多余的页框大小插入到对应的空闲页块链表中。

释放单页的内存时,内核将其置于CPU高速缓存中,对很可能出现在cache的页,则放到“快表”的列表中。在此过程中,内核先判断CPU高速缓存中的页数是否超过一定“阈值”,如果是,则将一批内存页还给伙伴系统,然后将该页添加到CPU高速缓存中。

释放多页的块时,内核首先计算出该内存块的伙伴的地址。内核将满足以下条件的三个块称为伙伴:(1)两个块具有相同的大小,记作b(2)它们的物理地址是连续的。(3)第一块的第一个页的物理地址2*(2^b)的倍数。如果找到了该内存块的伙伴,确保该伙伴的所有页都是空闲的,以便进行合并。内存继续检查合并后页块的“伙伴”并检查是否可以合并,依次类推。

  1. 内核将已分配页分为以下三种不同的类型:

不可移动页:这些页在内存中有固定的位置,不能够移动。

可回收页:这些页不能移动,但可以删除。内核在回收页占据了太多的内存时或者内存短缺时进行页面回收。

可移动页:这些页可以任意移动,用户空间应用程序使用的页都属于该类别。它们是通过页表映射的。当它们移动到新的位置,页表项也会相应的更新。

      1. I/O控制方式
  1. 直接I/O(轮询)

程序查询方式也称为程序轮询方式,该方式采用用户程序直接控制主机与外部设备之间输入/输出操作。CPU必须不停地循环测试I/O设备的状态端口,当发现设备处于准备好(Ready)状态时,CPU就可以与I/O设备进行数据存取操作。这种方式下的CPU与I/O设备是串行工作的,输入/输出一般以字节或字为单位进行。这个方式频繁地测试I/O设备,I/O设备的速度相对来说又很慢,极大地降低了CPU的处理效率,并且仅仅依靠测试设备状态位来进行数据传送,不能及时发现传输中的硬件错误。

  1. 中断

I/O设备结束(完成、特殊或异常)时,就会向CPU发出中断请求信号,CPU收到信号就可以采取相应措施。当某个进程要启动某个设备时,CPU就向相应的设备控制器发出一条设备I/O启动指令,然后CPU又返回做原来的工作。CPU与I/O设备可以并行工作,与程序查询方式相比,大大提高了CPU的利用率。但是在中断方式下,同程序查询方式一样,也是以字节或字为单位进行。但是该方法大大降低了CPU的效率,因为当中断发生的非常频繁的时候,系统需要进行频繁的中断源识别、保护现场、中断处理、恢复现场。这种方法对于以“块”为存取单位的块设备,效率是低下的。

  1. DMA

 DMA方式也称为直接主存存取方式,其思想是:允许主存储器和I/O设备之间通过“DMA控制器(DMAC)”直接进行批量数据交换,除了在数据传输开始和结束时,整个过程无须CPU的干预。每传输一个“块”数据只需要占用一个主存周期。

  1. 通道

 通道(Channel)也称为外围设备处理器、输入输出处理机,是相对于CPU而言的。是一个处理器。也能执行指令和由指令的程序,只不过通道执行的指令是与外部设备相关的指令。是一种实现主存与I/O设备进行直接数据交换的控制方式,与DMA控制方式相比,通道所需要的CPU控制更少,一个通道可以控制多个设备,并且能够一次进行多个不连续的数据块的存取交换,从而大大提高了计算机系统效率

      1. Spooling技术
  1. 假脱机系统; 在联机的情况下实现的同时外围操作的技术称为SPOOLing技术,或称为假脱机技术。 
  2. 组成
      1. 输入井和输出井:输入井和输出井的存储区域是在磁盘上开辟出来的。输入输出井中的数据一般以文件的形式组织管理,这些文件称之为井文件。一个文件仅存放某一个进程的输入或输出数据,所有进程的数据输入或输出文件链接成为一个输入输出队列。 
      2. 输入缓冲区和输出缓冲区:输入缓冲区和输出缓冲区的存储区域是在内存中开辟出来的。主要用于缓和CPU和磁盘之间速度不匹配的矛盾。输入缓冲区用于暂存有输入设备传送的数据,之后再传送到输入井;输出缓冲区 同理。 
      3. 输入进程和输出进程:输入进程也称为预输入进程,用于模拟脱机输入时的外围控制机,将用户要求的数据从输入设备传送到输入缓冲区,再存放到输入井。当CPU需要的时候,直接从输入井将数据读入内存。反之,输出的同理。 
      4. 井管理程序:用于控制作业与磁盘井之间信息的交换。
  3. 特点
  • 提高了I/O的速度:,对数据执行的I/O操作,已从对低速I/O设备执行的I/O操作演变为对磁盘缓冲区中数据的存取,如同脱机输入输出一样,提高了I/O速度,缓和了CPU和低速的I/Os设备之间速度的不匹配的矛盾。 
  • 将独占设备改造成了共享设备:因为在假脱机打印机系统中,实际上并没有为任何进程分配设备,而只是在磁盘缓冲区中为进程分配了一个空闲盘块和建立了一张I/O请求表。 
  • 实现了虚拟设备功能:宏观上,对于每一个进程而言,它们认为是自己独占了一个设备,即使实际上是多个进程在同时使用一台独占设备。也可以说,假脱机系统,实现了将独占设备变换为若干台对应的逻辑设备的功能。
      1. 通道技术
  1. 通道是独立于CPU,专门用来负责数据输入/输出传输工作的处理机,对外部设备实现统一管理,代替CPU对输入/输出操作进行控制,从而使输入,输出操作可与CPU并行操作。
  2. 引入通道的目的

为了使CPUI/O事务中解脱出来,同时为了提高CPU与设备,设备与设备之间的并行工作能力

      1. 共享内存的实现
  1. 两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
  2. 共享内存是通过把同一块内存分别映射到不同的进程空间中实现进程间通信。而共享内存本身不带任何互斥与同步机制,但当多个进程同时对同一内存进行读写操作时会破坏该内存的内容,所以,在实际中,同步与互斥机制需要用户来完成。
  3. (1)共享内存就是允许两个不想关的进程访问同一个内存 
    (2)共享内存是两个正在运行的进程之间共享和传递数据的最有效的方式 
    (3)不同进程之间共享的内存通常安排为同一段物理内存 
    (4)共享内存不提供任何互斥和同步机制,一般用信号量对临界资源进行保护。 
    (5)接口简单
      1. 计一个线程池,内存池
  1. 为什么需要线程池

大多数的网络服务器,包括Web服务器都具有一个特点,就是单位时间内必须处理数目巨大的连接请求,但是处理时间却是比较短的。在传统的多线程服务器模型中是这样实现的:一旦有个请求到达,就创建一个新的线程,由该线程执行任务,任务执行完毕之后,线程就退出。这就是”即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数非常频繁,那么服务器就将处于一个不停的创建线程和销毁线程的状态。这笔开销是不可忽略的,尤其是线程执行的时间非常非常短的情况。

  1. 线程池原理

应用程序启动之后,就马上创建一定数量的线程,放入空闲的队列中。这些线程都是处于阻塞状态,这些线程只占一点内存,不占用CPU。当任务到来后,线程池将选择一个空闲的线程,将任务传入此线程中运行。当所有的线程都处在处理任务的时候,线程池将自动创建一定的数量的新线程,用于处理更多的任务。执行任务完成之后线程并不退出,而是继续在线程池中等待下一次任务。当大部分线程处于阻塞状态时,线程池将自动销毁一部分的线程,回收系统资源。

  1. 线程池的作用

需要大量线程来完成任务,且完成任务的时间比较短性能要求苛刻的应用对性能要求苛刻的应用

  1. 内存池的原理

在软件开发中,有些对象使用非常频繁,那么我们可以预先在堆中实例化一些对象,我们把维护这些对象的结构叫“内存池”。在需要用的时候直接从内存池中拿,而不用从新实例化,在要销毁的时候,不是直接free/delete,而是返还给内存池。把那些常用的对象存在内存池中,就不用频繁的分配/回收内存,可以相对减少内存碎片,更重要的是实例化这样的对象更快,回收也更快。当内存池中的对象不够用的时候就扩容。

  1. 内存池的优缺点

内存池对象不是线程安全的,在多线程编程中,创建一个对象时必须加锁。

猜你喜欢

转载自blog.csdn.net/qq_34501451/article/details/83045450