操作系统

  • 说说进程/线程/协程

    • 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
    • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
    • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
    • 进程和线程的区别:

      • 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
      • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
      • 线程是处理器调度的基本单位,但进程不是
      • 二者均可并发执行
      • 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
    • 线程与协程的区别:

      • 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
      • 线程进程都是同步机制,而协程则是异步
      • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
    • https://blog.csdn.net/kowzb/article/details/77160249
  • 并发和并行的区别

    • 并发和并行是两种不同的概念。并行意味着程序在任意时刻都是同时运行的,并发意味着程序在单位时间内是同时运行的。并行就是在任一粒度时间内都具备同时执行的能力,最简单的并行就是多机,多台机器并行处理。SMP表面上看是并行的,但由于是共享内存,以及线程间的同步等,不可能完全做到并行。并发是在规定时间内多个请求都得到执行和处理,强调的是给外界的感觉,实际上内部可能是分时操作的。并发重在避免阻塞,使程序不会因为一个阻塞而停职处理。并发典型的应用场景:分时操作系统就是一种并发设计(忽略多核CPU)。
  • linux下创建进程的方法

    • linux下,创建进程主要是fork(), vfork(), clone()这三个函数。在linux源码中,这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。
    • fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的。但由于现在Linux中是采取了copy-on-write(写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。 fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。
    • 在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。
    • vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。 有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。 Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。
    • 系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。
    • 介绍一下fork的流程(从源码来看,fork就是简单的把父进程的几乎所有东西都拷贝一份,比如会复制父进程的地址空间、已打开文件描述符、命名空间啊这些之类的…然后修改一些标志让自己与父进程变得不一样,栈和堆会拷贝吗,会,复制之前会做些什么呢?task_struct吗?对对对,在这之前会先从slab中分配一个PCB…FIXME:copy-on-write没有表达出来)
    • 介绍一下fork、vfork还有clone的区别(从源码来分析,它们都用来创建linux轻量级进程的,vfork与fork的区别是,vfork共享父进程的地址空间,vfork之后父进程会让子进程先运行,因为vfork主要用于为了让子进程exec,exec之后子进程会用新程序的数据将内存重新刷一遍,这样它就有了自己的地址空间。子进程exec之后,会向父进程发送信号,这个时候父进程就可以开始运行了,如果子进程修改了父进程地址空间的话,父进程唤醒的时候就会发现自己的数据被改了,完整性丢失,所以这是不安全的。clone的话呢,它提供选项,让你自己选择每次复制哪些东西,但是它调用的还是do_fork好像)
    • 从执行顺序上看,fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。
  • 多进程与多线程的区别

    • 多进程优点

      • 更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
      • 编程相对容易;通常不需要考虑锁和同步资源的问题。
      • 有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。
    • 多进程缺点

      • 逻辑控制复杂,需要和主程序交互
      • 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
      • 多进程调度开销比较大
    • 多线程优点

      • 无需跨进程边界
      • 程序逻辑和控制方式简单
      • 所有线程可以直接共享内存和变量等
      • 线程方式消耗的总资源比进程方式好
    • 多线程缺点

      • 每个线程与主程序共用地址空间,受限于2GB地址空间
      • 线程之间的同步和加锁控制比较麻烦
      • 一个线程的崩溃可能影响到整个程序的稳定性
      • 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
  • C++ 创建线程

    • 主要是用POSIX那套,pthread_create
  • 读者-写者问题

    • 在系统中,一个数据集(如文件或记录) 被几个并发进程共享,这些线程分两类,一部分只要求进行读操作,称之为“读者”; 另一类要求写或修改操作,我们称之为“写者“。一般而言,对一个数据集,为了保证数据的完整性、正确性,允许多个读者进程同时访问,但是不允许一个写者进程同其它任何一个进程(读者或者写者)同时访问,而这类问题就称之为”读者-写者”问题。
    • 读者优先的算法在操作系统相关的书籍中都有介绍,这是一种最简单的解决办法: 当没有写进程正在访问共享数据集时,读进程可以进入访问,否则必须等待。而读者优先的算法存在”饿死写者”线程的问题:只要有读者不断到来,写者就要持久地等待,直到所有的读者都读完且没有新的读者到来时写者才能写数据集。而在很多情况下我们需要避免”饿死写者”,故而采用写者优先算法
    • 写者优先算法中,1.要让读者与写者之间、以及写者与写者之问要互斥地访同数据集; 2.在无写进程到来时各读者可同时访问数据集; 3.在读者和写者都等待时访问时写者优先
    • 在实现写者优先时,增加一个互斥量,用于写者优先。当有写者来时,就不在允许读者去读取数据, 等待正在读数据的读者完成以后开始写数据,以此实现写者优先。
  • 生产者消费者问题

    • 也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
    • 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
    • 需要保持线程间的同步,即一个线程消费(或生产)完,其他线程才能进行竞争CPU,获得消费(或生产)的机会。对于这一点,可以使用条件变量进行线程间的同步:生产者线程在product之前,需要wait直至获取自己所需的信号量之后,才会进行product的操作;同样,对于消费者线程,在consume之前需要wait直到没有线程在访问共享区(缓冲区),再进行consume的操作,之后再解锁并唤醒其他可用阻塞线程。
    • 在访问共享区资源时,为避免多个线程同时访问资源造成混乱,需要对共享资源加锁,从而保证某一时刻只有一个线程在访问共享资源。
  • 哲学家就餐问题

  • 5个哲学家围坐在桌子周围,每人左边一根筷子,共5根筷子,平时在思考,吃饭时,只有同时拿起左、右2根筷子的哲学家才可以就餐,吃完饭,放下筷子。一般情况下没有问题,但在竟态情况下,比如,每个人只能竞争到左边或右边的一根筷子时,都不能得到2根筷子,就出现了都不能吃饭问题。

  • 线程安全吗?

    • 通常讲的是针对方法或者函数,在函数执行过程中不会造成资源冲突就是线程安全的,多个线程来调用也没事情,线程不安全就会造成数据错误或者崩溃啊啥的。
    • 线程安全: 在多线程中使用时,不用自已做同步处理. 线程不安全: 在多线程中使用时, 必须做线程同步,不然会有未知后果. 对于线程不安全的代码, 注意做好互斥与同步, 对于异常处理要完善.
    • 如果调用某个接口时需要我们自己采取同步措施来保护该接口访问的共享资源,则这样的接口不是线程安全的. MFC和STL都不是线程安全的. 如果接口中访问的数据都属于私有数据,那么这样的接口是线程安全的.或者几个接口对共享数据都是只读操作,那么这样的接口也是线程安全的.如果多个接口之间有共享数据,而且有读有写的话,如果设计者自己采取了同步措施,调用者不需要考虑数据同步问题,则这样的接口是线程安全的,否则不是线程安全的。
  • 操作系统的进程调度策略

    • 先来先服务调度算法:先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
    • 短作业(进程)优先调度算法:短作业(进程)优先调度算法SJ(P)F,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
    • 高优先权优先调度算法:为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。

      • 非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
      • 抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
      • 容易出现优先级倒置现象:优先级反转是指一个低优先级的任务持有一个被高优先级任务所需要的共享资源。高优先任务由于因资源缺乏而处于受阻状态,一直等到低优先级任务释放资源为止。而低优先级获得的CPU时间少,如果此时有优先级处于两者之间的任务,并且不需要那个共享资源,则该中优先级的任务反而超过这两个任务而获得CPU时间。如果高优先级等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源,因为此时低优先级进程无法与高优先级进程争夺CPU时间,从而无法执行,进而无法释放资源,造成的后果就是高优先级任务无法获得资源而继续推进。
    • 高响应比优先调度算法:在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
    • 时间片轮转法:在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。
    • 多级反馈队列调度算法:前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

      • 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
      • 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
      • 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
    • 批处理系统常用调度算法:FCFS、短作业优先、最短剩余时间优先、高响应比优先
    • 分时系统调度算法:轮转调度、优先级调度多级队列调度、彩票调度
    • 实时系统:单比率调度、限期调度、最少裕度法
  • 堆是线程共有还是私有,堆是进程共有还是私有,栈呢

    • 在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享。
  • 如何安全的析构线程

    • 作为 class 数据成员的 Mutex 只能用于同步本 class 的其他数据成员的读和写,它不能保护安全地析构。因为成员 mutex 的生命期最多与对象一样长,而析构动作可说是发生在对象身故之后(或者身亡之时)。另外,对于基类对象,那么调用到基类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的 mutex 不能保护整个析构过程。再说,析构过程本来也不需要保护,因为只有别的线程都访问不到这个对象时,析构才是安全的。
    • 正确做法是使用shared_ptr。shared_ptr控制对象的生命期。shared_ptr 是强引用(想象成用铁丝绑住堆上的对象),只要有一个指向 x 对象的 shared_ptr 存在,该 x 对象就不会析构。当指向对象 x 的最后一个 shared_ptr 析构或 reset 的时候,x 保证会被销毁。weak_ptr不控制对象的生命期,但是它知道对象是否还活着(想象成用棉线轻轻拴住堆上的对象)。如果对象还活着,那么它可以提升 (promote) 为有效的 shared_ptr;如果对象已经死了,提升会失败,返回一个空的 shared_ptr。shared_ptr/weak_ptr 的“计数”在主流平台上是原子操作,没有用锁,性能不俗。
  • 多进程、多线程的使用场景

    • 多进程

      • nginx主流的工作模式是多进程模式
      • 几乎所有的web server服务器服务都有多进程的,至少有一个守护进程配合一个worker进程,例如apached,httpd等等以d结尾的进程包括init.d本身就是0级总进程,所有你认知的进程都是它的子进程
      • chrome浏览器也是多进程方式
      • redis也可以归类到“多进程单线程”模型
    • 多线程

      • 线程间有数据共享,并且数据是需要修改的
      • 提供非均质的服务(有优先级任务处理)事件响应有优先级
      • 单任务并行计算,在非CPU Bound的场景下提高响应速度,降低时延
      • 与人有IO交互的应用,良好的用户体验(键盘鼠标的输入,立刻响应)
    • 如何选择

      • 需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
      • 需要进行大量计算的优先使用线程(CPU频繁切换)
      • 强相关的处理用线程,弱相关的处理用进程
      • 可能要扩展到多机分布的用进程,多核分布的用线程
  • 一个进程三个线程与三个进程的区别,优劣。

    • 前者轻,省空间;后者重,占用空间大
    • 前者鲁棒性弱,后者鲁棒性强
    • 前者上下文切换代价小,后者大
    • 前者可以通过共享内存通信,后者不行
  • 多线程会有什么问题?

    • 原子性问题

      • 所谓原子性,指的是一个操作不可中断,即在多线程并发的环境下,一个操作一旦开始,就会在同一个CPU时间片内执行完毕。如果同一个线程的多个操作在不同的CPU时间片上执行,由于中间出现停滞,后面的操作在执行时可能某个共享数据被其他线程修改,而该修改并未同步到当前线程中,导致当前线程操作的数据与实际不符,这种由于执行不连贯导致的数据不一致问题被称作原子性问题。
    • 可见性问题

      • 可见性问题的出现与线程访问共享数据的方式有关。线程访问堆(方法区)中的变量时,先在栈中建立一个变量的副本,修改后再同步到堆中。如果一个线程刚建立副本,这时另一线程修改了变量,尚未同步到堆中,这时就会出现两个线程操作同一变量的同一种状态的现象
    • 有序性问题

      • 为了提高执行效率,CPU会对那些没有依赖关系的指令重新排序,重新排序后的执行结果与顺序执行结果相同。指令重排在单线程环境下是安全的,在多线程环境下就可能出现问题。
    • put的时候如果扩容,会导致形成循环链表,这样get的时候就会陷入死循环。还有大部分并发都存在的问题吧,临界区资源没有上锁会导致脏读、丢失更新什么的。就两个线程同时put同一个key,那有一个线程的value就丢失更新了。或者某个线程同时get了某个key,这个key又被其他线程修改了,脏读了。
  • 僵尸进程

    • 在Linux系统中,任何一个子进程在调用exit()函数结束运行后,内核会释放该进程的所有资源,包括占用的内存和打开的文件等。
    • 同时,也会留下一个叫做僵尸进程(Zombie)的数据结构,Zombie中存储了该进程的进程号、退出码、退出状态、使用的CPU时间等信息。即僵尸进程是早已死亡的子进程,但在进程表中占了一个位置(slot)。
    • 子进程还会向父进程发送SIGCHLD信号,父进程调用wait()或者waitpid()函数可以将僵尸进程释放(为它收尸)。若父进程在没有释放掉僵尸进程就提前结束了,僵尸进程则会由init进程接管。init进程(PID = 1)会作为它的父进程,为它收尸。但是如果父进程是一个循环,不会结束,却又没有为SIGCHLD信号绑定处理函数,或者没有调用wait()/waitpid()函数为僵尸进程收尸,则该僵尸进程会一直在系统中存在。
    • 僵尸进程的危害

      • 如果系统中存在很多僵尸进程,进程号会被它们一直占用。这时,有限的进程号将会耗尽,使得系统无法创建新的进程。
    • 怎么看僵尸进程

      • ps -ef | grep defunct
    • 怎么避免僵尸进程

      • 父进程收到SIGCHLD信号后,调用wait()或者waitpid()函数释放僵尸进程。但是,这样会导致父进程挂起。
      • 如果父进程很忙,可以使用signal函数为SIGCHLD信号安装handler,handler中会调用wait函数回收僵尸进程。子进程结束后,父进程收到SIGCHLD信号后会执行handler。
      • 父进程显式地表示自己对子进程的结束不感兴趣

        • 父进程如果不关心子进程什么时候结束,可以通过signal(SIGCLD, SIG_IGN)或者signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣。它们的区别是SIGCLD在安装完信号处理函数的时候还会检查是否已经存在结束的子进程,如果有就调用信号处理函数,而SIGCHLD不会,也就是可能会丢掉已经有子进程已经结束这个事实
        • 子进程结束后,内核会释放僵尸进程,并不在给父进程发送信号
  • 孤儿进程 orphan

    • 父进程退出,而它的一个或多个子进程还在运行,这些子进程将成为孤儿进程(orphan process)。
    • 孤儿进程将会被init进程收养,并由init进程完成对它们的状态收集工作。
    • 由于孤儿进程会被init进程收养,因此孤儿进程调用exit()结束运行时,也会由init进程完成回收工作,而不会对系统造成危害。
  • 守护进程 daemon

    • 是运行在后台的一种特殊进程,独立于控制终端,并且周期性的执行某种任务或者等待处理某些发生的事件。
    • 守护进程不需要用户输入就能运行,它可以提供某种服务。并且不是对系统提供服务,就是对某个应用程序提供服务。
    • 守护进程一般在系统启动时就开启了,除非强制终止,否则直到系统关机都保持运行。
    • 守护进程的父进程是init进程: 创建守护进程时,父进程在fork出子进程后就提前结束运行了。守护进程将会变成孤儿进程,由init进程收养。
    • 守护进程是非交互式程序,没有控制终端,无论是标准输出设备stdout还是标准出错设备stderr的输出都需要进行特殊处理。
    • 守护进程的名称通常以d结尾,如sshd、xinetd、crond等。
  • 死锁产生原因、避免、检测与解决

    • 所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
    • 产生死锁的原因

      • 资源竞争。系统资源分为可剥夺和不可剥夺,产生死锁中的竞争资源之一指的是竞争不可剥夺资源,另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
      • 进程间推进顺序非法
    • 产生死锁的必要条件

      • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用
      • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
      • 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
      • 环路等待条件:在发生死锁时,必然存在一个进程—资源的环形链
    • 预防死锁

      • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了
      • 只要有一个资源得不到分配,也不给这个进程分配其他的资源
      • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源
      • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反
    • 避免死锁

      • 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
      • 为实现银行家算法,每一个新进程在进入系统时,必须申明在运行过程中可能需要每种资源类型的最大单元数目,其数目不应超过系统所拥有的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程。若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态。如果不会,才将资源分配给它,否则让进程等待。首先为实现银行家算法,在系统中必须设置这样四个数据结构:

        • 可利用资源向量Avaliable。这是一个含有m个元素的数组,其中每一个元素代表一类可利用的资源数目,其初始值是系统所配置的该类全部可用资源的数目,其中的每一个元素代表一类可利用的资源数目,其初始值是系统中所配置的该类全部可用资源的数目,其数值随该类资源的分配和回收而动态的改变。如果Available[j]=K,则表示系统中现有Rj类资源的最大数目为K。
        • 最大需求矩阵Max。是一个n×m的矩阵,定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i, j]=K,则表示进程i需要Rj类资源的最大数目为K。
        • 分配矩阵Allocation。是一个n×m的矩阵,定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i, j]=K,则表示进程i当前已分得Rj类资源的数目为K。
        • 需求矩阵Need。是一个n×m的矩阵,用以表示每一个进程尚需的各类资源数。如果Need[i, j]=K,则表示进程i还需要Rj类资源K个方能完成其任务。它们之间的关系为:Need[i, j]=Max[i, j]-Allocation[i, j]
      • 银行家算法描述如下:

        • 设置两个向量

          • 工作向量Work,表示系统可提供给进程继续运行所需的各类资源数目,含有m个元素,在执行安全算法开始时,Work=Available
          • Finish,表示系统是否有足够的资源分配给进程,使之运行完成。先令Finish[i]=false;当有足够资源分配给进程时,令Finish[i]=true
        • 从进程集合中找到一个能满足下述条件的进程:

          • Finish[i]=false
          • Need[i, j]≤Work[j]
          • 若找到,执行下一步,否则执行下下一步
        • 当进程Pi获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故执行:

          • Work[j]=Work[j]-Allocation[i, j]
          • Finish[i]=true
          • 返回上一步
        • 如果所有进程的Finish[i]=true都满足,则表示系统处于安全状态,否则,系统处于不安全状态
    • 死锁检测

      • 首先为每个进程和每个资源指定一个唯一的号码
      • 然后建立资源分配表和进程等待表
    • 死锁解除

      • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态
      • 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等
  • 进程上下文切换怎么实现

    • 进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。这里所说的从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中,从而把处理器的寄存器腾出来让其他进程使用。那么被中止运行进程的中间数据存在何处好呢?当然这个地方应该是进程的私有堆栈。
    • 让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
    • 在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程的上下文是存储在进程的私有堆栈中的。
    • 显然,进程的切换可以用中断技术来实现,即当调度器获得了待运行进程的控制块之后,应立即用软中断指令来中止当前进程的运行,并保存当前进程的PC值和PSW值。其后,使用压栈指令把处理器其他寄存器的值压入进程私有堆栈。接下来,就从待运行进程的进程控制块中取出私有堆栈指针的值并存入处理器的寄存器SP,至此SP就指向了待运行进程的私有堆栈,于是下面就自待运行进程的私有堆栈中弹出上下文进入处理器。最后,利用中断返回指令来实现自待运行进程的私有堆栈中弹出PSW值和自待运行进程的 私有堆栈中弹出PC值的功能。
  • PV操作

    • PV操作是有名的计算机科学家狄克斯特拉为了解决一类问题而创造的,例如:假如P1和P2是分别将数据送入缓冲B和从缓冲B读出数据的两个进程,为了防止这两个进程并发时产生错误,狄克斯特拉设计了一种同步机制叫“PV操作”。PV操作有P操作和V操作组成,它们是两个不可中断的过程,也叫做原语。它是为了能够实现对于并发进程中临界区的管理要求。
    • 为什么要有PV操作?是为了防止两个进程并发时产生错误。这里不得不说的就是,并发进程之间分为两种,一种就是有交互的,一种是无任何关联的。没有关联的并发进程是相互独立的,谁也不影响谁。但是交互的并发进程可就不一样了,因为他们是共享资源的,一个进程运行时,经常会由于自身或外界的原因而被中端,且断点是不固定的。也就是说进程执行的相对速度不能由进程自己来控制,于是就会导致并发进程在共享资源的时出现与时间有关的错误。
    • 我们把并发进程中与共享变量有关的程序段称为临界区。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
    • P操作:将信号量S减去1,若结果小于0,则把调用P(S)的进程置成等待信号量S的状态。即为请求资源。
    • V操作:将信号量S加上1,若结果不大于0,则释放一个等待信号量S的进程。即为释放资源。
  • 进程间通信

    • 多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程使用,我们把一次仅允许一个进程使用的资源成为临界资源。许多物理设备都属于临界资源,如打印机等。对临界资源的访问,必须互斥的进行,在每个进程中,访问临界资源的那段代码成为临界区(critical section)。
    • 进程间通信和同步有如下目的:

      • 数据传输:一个进程需要将它的数据发送给另一个进程。
      • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
      • 通知事件:一个进程需要向另一个或另一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
      • 资源共享:多个进程之间共享相同的资源。为了做到这一点,需要内核提供锁和同步机制
      • 进程控制;有些进程希望完全控制另一个进程的执行(如Degbug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态变化
    • 通信手段

      • 信号量(semaphore) :主要作为进程间以及同一进程不同线程之间的同步手段。
      • 共享内存:使得多个进程可以访问同一块内存空间,是最快的IPC形式。是针对其他通信机制运行效率低而设计的。往往与其他通信机制,如信号量结合使用,来达到进程间的同步与互斥。
      • Message(消息队列):消息队列是消息的链表,包括Posix消息队列System V消息队列。有足够权限的进程可以向消息队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息少,管道承载无格式字节流以及缓冲区大小受限等缺点。
      • 管道(pipe)与有名管道(named pipe):管道只能在具有公共祖先的两个进程间使用。有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
      • 信号(signal):信号时比较复杂的通信方式,用于通知接受进程有关某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。
      • 套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信.起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其他类unix系统上:Linux和System V的变种都支持套接字。
  • 线程间通信

    • 通信手段

      • 信号量
      • 互斥量

        • 信号量有计数能力,互斥量是信号量的一个简化版本。互斥量是一个处于两个状态之一的变量:解锁和加锁。这样,只需要一个二进制位表示它,不过实际上,常常使用一个整形量,0表示解锁,而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)需要访问临界区时,它调用mutex_lock。如果互斥量当前是解锁的(即临界区可用)。此调用成功,调用线程可以进入该临界区。另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在互斥量上,将随机选择一个线程并允许它获得锁。一个线程如果想要进入临界区,它首先尝试锁住相关的互斥量。如果互斥量没有加锁,那么这个进程可以立即进入,并且该互斥量被自动锁住以防止其他线程进入。如果互斥量已经被加锁,则调用线程被阻塞,直到该互斥量被解锁。如果多个线程在等待同一个互斥量,当它被解锁时,这些等待的线程中只有一个被允许运行并将互斥量重新锁定。这些互斥量不是强制的,而是由程序员来保证线程正确地使用它们。
      • 条件变量

        • 条件变量允许一个进程在需要的条件未达到时阻塞自己并在以后被唤醒。POSIX中与条件变量相关的最重要的两个操作是pthread_cond_wait和pthread_cond_signal。前者阻塞调用线程直到另一其他线程向它发信号(使用后一个调用)。当然,阻塞与等待的原因不是等待与发信号协议的一部分。被阻塞的线程经常是在等待发信号的线程去做某些工作、释放某些资源或是进行其他的一些活动。只有完成后被阻塞的线程才可以继续运行。条件变量允许这种等待与阻塞原子性地进行。互斥量和条件变量经常一起使用。这种模式用于让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量。最后另一个线程会向他发信号,使它可以继续运行。
  • 共享内存底层是怎么实现的

    • 两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
    • https://blog.csdn.net/Al_xin/article/details/38602093
  • 解释一下内存池的概念

    • 内存池是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
    • 内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
    • 应用程序自定义的内存池根据不同的适用场景又有不同的类型。从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更加广泛。从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。
    • 内存池技术因为其对内存管理有着显著的优点,在各大项目中广泛应用,备受推崇。但是,通用的内存管理机制要考虑很多复杂的具体情况,如多线程安全等,难以对算法做有效的优化,所以,在一些特殊场合,实现特定应用环境的内存池在一定程度上能够提高内存管理的效率。
    • 经典内存池技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。既然是针对特定对象的内存池,所以内存池一般设置为类模板,根据不同的对象来进行实例化。
    • 经典内存池实现过程

      • 先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象
      • 每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间
      • 某个内存节点一旦分配出去,从空闲内存节点链表中去除
      • 一旦释放了某个内存节点的空间,又将该节点重新加入空闲内存节点链表
      • 如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中
    • 内存池特征

      • 无内存泄漏:正确的使用内存池的申请和释放函数不会造成内存泄露,更重要的是,即使不正确的使用了申请和释放函数,内存池中的内存也会在进程结束时被全部自动释放,不会造成系统的内存泄露。
      • 申请的内存数组没有被填充:例如一个元素的内存大小为A,那么元素数组若包含n个元素,则该数组的内存大小必然是A*n,不会有多余的内存来填充该数组。尽管每个元素也许包含一些填充的东西。
      • 任何数组内存块的位置都和使用operator new[]分配的内存块位置一致,这表明你仍可以使用那些通过数组指针计算内存块位置的算法。
      • 内存池要比直接使用系统的动态内存分配快。这个快是概率意义上的,不是每个时刻,每种内存池都比直接使用new或者malloc快。例如,当程序使用内存池时内存池恰好处于已经满了的状态,那么这次内存申请会导致内存池自我扩充,肯定比直接new一块内存要慢。但在大部分时候,内存池要比new或者malloc快很多。
  • 有哪些内存池

    • 目前我只知道C++有boost内存池,其实是个轮子
    • 早期的内存池技术是为了专门解决那种频繁申请和释放相同大小内存块的程序,因此早期的一些内存池都是用相同大小的内存块链表组织起来的。
    • Boost的内存池则对内存块的大小是否相同没有限制,因此只要是频繁动态申请释放内存的长时间运行程序,都适用Boost内存池。这样可以有效减少内存碎片并提高程序运行效率。
  • 内存碎片(外碎片和内碎片)

    • 描述一个系统中所有不可用的空闲内存。这些资源之所以仍然未被使用,是因为负责分配内存的分配器使这些内存无法使用。这一问题通常都会发生,原因在于空闲内存以小而不连续方式出现在不同的位置。由于分 配方法决定内存碎片是否是一个问题,因此内存分配器在保证空闲资源可用性方面扮演着重要的角色。
    • 内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。
    • 外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。
    • 固定分区存在内部碎片,可变式分区分配会存在外部碎片
    • 页式虚拟存储系统存在内部碎片;段式虚拟存储系统,存在外部碎片
    • 为了有效的利用内存,使内存产生更少的碎片,要对内存分页,内存以页为单位来使用,最后一页往往装不满,于是形成了内部碎片。
    • 为了共享要分段,在段的换入换出时形成外部碎片,比如5K的段换出后,有一个4k的段进来放到原来5k的地方,于是形成1k的外部碎片。
  • 内存为程序分配空间有四种分配方式

    • 连续分配方式
    • 基本分页存储管理方式
    • 基本分段存储管理方式
    • 段页式存储管理方式
    • 详细
  • 你知道的锁有哪些, 有什么区别。有哪些自旋锁,分别是怎么实现的

    • 互斥锁

      • 互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子API之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑
    • 信号量

      • Linux内核的信号量在概念和原理上与用户态的SystemV的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与SystemV的IPC机制信号量毫不相干。
      • 信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为0,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为正数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
    • 读写锁。也叫作共享式互斥锁,读写信号量

      • 有3种状态:读模式的加锁状态、写模式的加锁状态、不加锁状态。
      • 写模式加锁状态:在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
      • 读模式加锁状态:所有试图以读模式进行加锁的线程都可以得到访问权,但是任何希望以写模式对此加锁的线程都会阻塞,直到所有的线程释放他们的读锁为止。
    • 顺序锁

      • 跟读写锁很像,用于能够区分读与写的场合,并且是读操作很多、写操作很少,写操作的优先权大于读操作。
      • seqlock的实现思路是,用一个递增的整型数表示sequence。写操作进入临界区时,sequence++;退出临界区时,sequence再++。写操作还需要获得一个锁(比如mutex),这个锁仅用于写写互斥,以保证同一时间最多只有一个正在进行的写操作。
        当sequence为奇数时,表示有写操作正在进行,这时读操作要进入临界区需要等待,直到sequence变为偶数。读操作进入临界区时,需要记录下当前sequence的值,等它退出临界区的时候用记录的sequence与当前sequence做比较,不相等则表示在读操作进入临界区期间发生了写操作,这时候读操作读到的东西是无效的,需要返回重试。
      • seqlock写写是必须要互斥的。但是seqlock的应用场景本身就是读多写少的情况,写冲突的概率是很低的。所以这里的写写互斥基本上不会有什么性能损失。
        而读写操作是不需要互斥的。seqlock的应用场景是写操作优先于读操作,对于写操作来说,几乎是没有阻塞的(除非发生写写冲突这一小概率事件),只需要做sequence++这一附加动作。而读操作也不需要阻塞,只是当发现读写冲突时需要retry。
    • 自旋锁

      • 与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
      • 由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
      • 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
      • 如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
      • 自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
      • 跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
    • RCU

      • RCU也是用于能够区分读与写的场合,并且也是读多写少,但是读操作的优先权大于写操作(与seqlock相反)。
      • RCU的实现思路是,读操作不需要互斥、不需要阻塞、也不需要原子指令,直接读就行了。而写操作在进行之前需要把被写的对象copy一份,写完之后再更新回去。其实RCU所能保护的并不是任意的临界区,它只能保护由指针指向的对象(而不保护指针本身)。读操作通过这个指针来访问对象(这个对象就是临界区);写操作把对象复制一份,然后更新,最后修改指针使其指向新的对象。由于指针总是一个字长的,对它的读写对于CPU来说总是原子的,所以不用担心更新指针只更新到一半就被读取的情况(指针的值为0x11111111,要更新为0x22222222,不会出现类似0x11112222这样的中间状态)。所以,当读写操作同时发生时,读操作要么读到指针的旧值,引用了更新前的对象、要么读到了指针的新值,引用了更新后的对象。即使同时有多个写操作发生也没关系(是否需要写写互斥跟写操作本身的场景相关)。
  • Linux的虚拟地址空间布局是怎么样的。内存分段机制(代码段、数据段、BSS、堆、栈)以及每个段的作用

    • 虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

    • 分段机制是为了解决冲突问题,它是一种机制,这种机制使得很方便地管理内存

    • 分段是一个逻辑实体。一个段中可以是变量,源代码或者堆栈。一般来说每个段中不会包含不同类型的内容。而分段主要有以下几个作用:

      • 解决编译问题: 前面提到过在编译时地址覆盖的问题,可以通过分段来解决,从而简化编译程序。
      • 重新编译: 因为不同类型的数据在不同的段中,但其中一个段进行修改后,就不需要所有的段都重新进行编译。
      • 内存共享: 对内存分段,可以很容易把其中的代码段或数据段共享给其他程序,分页中因为数据代码混合在一个页面中,所以不便于共享。
      • 安全性: 将内存分为不同的段之后,因为不同段的内容类型不同,所以他们能进行的操作也不同,比如代码段的内容被加载后就不应该允许写的操作,因为这样会改变程序的行为。而在分页系统中,因为一个页不是一个逻辑实体,代码和数据可能混合在一起,无法进行安全上的控制。
      • 动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
      • 保持兼容性
    • 每个段的作用

      • | 名称 | 存储内容 | | —- | —- | | 栈 | 局部变量、函数参数、返回地址等 | | 堆 | 动态分配的内存 | | BSS段 | 未初始化或初值为0的全局变量和静态局部变量 | | 数据段 | 已初始化且初值非0的全局变量和静态局部变量 | | 代码段 | 可执行代码、字符串字面值、只读变量 |
  1. -

在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。

  • wait sleep的区别,并说明使用场景,wait sleep都会释放cpu资源吗

    • 这题是java的东西,但知道一下也无妨
    • 这两个方法来自不同的类,sleep来自Thread类,和wait来自Object类。
    • sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
    • sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    • sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
    • sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
    • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  • 系统调用或者说中断的过程,中断的作用是什么,比如说任务调度?软中断 硬中断

    • 中断是指程序执行过程中,遇到急需处理的事件时,暂时中止CPU上现行程序的运行,转去执行相应的事件处理程序,待处理完成后再返回原程序被中断处或调度其他程序执行的过程。操作系统是“中断驱动”的;换言之,中断是激活操作系统的唯一方式。
    • 中断或异常处理执行的代码不是一个进程,而是内核控制路径,它代表异常或中断发生时正在运行的当前进程在内核态执行一个独立的指令序列。内核控制路径比进程更“轻”,其上下文信息比进程上下文信息少得多。而上下文切换后CPU执行的是另一个用户进程。
    • 根据中断源不同,中断事件处理原则为

      • 处理器硬件故障中断:由处理器、内存储器、总线等硬件故障引起。会通过中断请求向CPU请求处理。处理原则为:保护现场,停止设备,停止CPU,向操作员报告并对故障造成的破坏进行估计和恢复,等待人工干预(复位、设置、替换等)
      • 程序性中断:由处理器执行机器指令引起。不同用户往往有不同处理要求,借助于信号机制,操作系统可将所捕获的中断事件原封不动的转交给应用程序自行处理。比如程序运行过程中产生的异常。
      • 访管中断:处理器请求分配外设、请求I/O等时,执行访管指令请求OS服务引起(系统调用通过访管指令和中断机制实现)
      • I/O中断事件:来源于外围设备报告I/O状态的中断事件
      • 时钟中断:由外围设备发出的信号引起的中断事件
    • 中断源可以分为两部分:本地中断源和外部中断源。本地中断源有些场合又称为软件中断,因为没有具体的硬件与之对应。而那些由具体硬件触发的中断则称为硬件中断。而异常则是程序指令流执行过程中的同步过程,比如程序执行过程中遇到除零错,很显然此时程序无法继续运行,只能处理完了这个异常,才可以继续运行。异常的同步特性和中断的异步又是一个明显的区别。另外在linux中为了让内核延期执行某个任务,也提出了一个软中断(software interrupt)的概念,这点在windows中与之对应的机制为DPC,即延迟过程调用。
  • 内存延迟分配怎么实现的,内存分配的系统调用是什么

    • Linux内存的延迟分配就是在你未使用内存(均值物理内存)的时候,操作系统是不会真正的分配物理内存的。只有某一页上的内存被访问,则该页才会被实际的映射到物理内存上来,也即开始占用物理内存。
  • Linux的namespace

    • namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。
    • Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
    • Linux 内核实现 namespace 的一个主要目的就是实现轻量级虚拟化(容器)服务。在同一个 namespace 下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,认为自己置身于一个独立的系统中,从而达到隔离的目的。也就是说 linux 内核提供的 namespace 技术为 docker 等容器技术的出现和发展提供了基础条件。
  • 用户态和内核态的区别(一般是从特权级考虑的)

    • 对于任何操作系统来说,创建一个进程是核心功能。创建进程要做很多工作,会消耗很多物理资源。比如分配物理内存,父子进程拷贝信息,拷贝设置页目录页表等等,这些工作得由特定的进程去做,所以就有了特权级别的概念。最关键的工作必须交给特权级最高的进程去执行,这样可以做到集中管理,减少有限资源的访问和使用冲突。inter x86架构的cpu一共有四个级别,0-3级,0级特权级最高,3级特权级最低。
    • 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。
    • 用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。
    • 当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。
    • 用户态切换到内核态的3种方式

      • 系统调用:这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。例如fork()就是执行了一个创建新进程的系统调用。系统调用的机制和新是使用了操作系统为用户特别开放的一个中断来实现,如Linux的int 80h中断。
      • 异常:当cpu在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。
      • 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行,如果前面执行的指令时用户态下的程序,那么转换的过程自然就会是 由用户态到内核态的切换。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。
      • 这三种方式是系统在运行时由用户态切换到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。从触发方式上看,切换方式都不一样,但从最终实际完成由用户态到内核态的切换操作来看,步骤又是一样的,都相当于执行了一个中断响应的过程。系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本一致。
  • 端口

    • 用来区分同一主机上的不同服务。端口号是标识主机内唯一的一个进程,IP+端口号就可以标识网络中的唯一进程。在我们通常用的Socket编程中,IP+端口号就是套接字
    • 分类

      • 服务端使用的端口号

        • 预留端口号。取值范围0-1023,这些端口我们编程的时候不能使用,是那些vip应用程序使用的,只有超级用户特权的应用才允许被分配一个预留端口号
        • 登记端口号。取值范围1024-49151,就是我们平时编写服务器使用的端口号范围
      • 客户端

        • 取值范围49152-65535,这部分是客户端进程运行时动态选择的范围,又叫临时端口号
  • linux服务器性能监测

  • 说一些你常用的linux命令。

  • 写出你熟悉的Linux命令
    文件:ls ll -a la
    进程和文件:ps,top,df,du,pstree (top命令都能看到哪些信息?)
    网络:netstat,ping,curl,traceroute,iptables
    文本:cat,vi,wc,grep,awk,sed,head,tail,more,less,
    查找:whereis find -n locate which
    权限:chmod chown 777 su sudo
    定时任务和后台执行:crontab nohup &后台执行

  • 查询进程的相关指令:ps -aux/ps -ef

  • 查询相关端口的指令:netstat -ntl

  • 查询内存使用状态:free -h ,显示的各个字段的含义

  • Liunx下如何查看共享内存的情况:ipcs -m

  • 基本的操作:复制,删除,重命名,移动

  • 设置开启自启动,设计定时执行

  • Stat,trace,抓包用什么命令 tcpdump

  • 防火墙的设置

  • linux中如何查看一个端口是否被占用,被那个进程占用。

  • http的默认端口是80,怎么将它改为8080端口。

    • 打开$HTTPD_HOME/conf/httpd.conf文件,找到Listen,后面紧跟的是端口号,默认是80,把它修改为你想设置的端口号即可。如果不知道Apache的安装目录,可以用locate httpd.conf命令来查找。
  • linux中vim怎么统计文件的行数。

    • 如果只是linux下,用wc -l filename
    • :%s/./&/gn 统计字符数
    • :%s/\i+/&/gn 统计单词数
    • :%s/^//n 统计行数
    • :%s/keyword/&/gn 统计任何地方出现的 “keyword”
  • Linux下查看内存使用命令是什么?查看负载的命令是什么?

  • Linux下如何查看服务器网络状态? ifconfig、netstat -an

  • 服务器CPU 100%怎么定位

    • 先用top定位出问题的进程,再用top定位到出问题的线程,再打印线程堆栈查看运行情况
    • top jstack,日志,gui工具
  • 怎么定位内存泄漏(用jmap把内存dump下来,然后再用工具分析堆栈)

  • Linux大文件怎么查某一行的内容:sed -n ‘x,yp’ filename就可以看第x行到第y行

  • Linux下TCP服务器都有什么状态?

    • ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
  • Linux下TIME_WAIT和CLOSE_WAIT区别是什么?

    • close_wait是被动关闭, time_wait是主动关闭
  • too many files是怎么产生的,用什么方法可以发现这个问题,怎么解决这个问题

    • TIME_WAIT和CLOSE_WAIT两种状态如果一直被保持,那么意味着对应数目的通道就一直被占着,而且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就无法被处理了,接着就是大量Too Many Open Files异常,tomcat崩溃
    • 发现问题:用命令netstat -n | awk **'/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'**
    • 保持time_wait的解决:

      • 这种情况比较常见,一些爬虫服务器或者WEB服务器(如果网管在安装的时候没有做内核参数优化的话)上经常会遇到这个问题。TIME_WAIT是主动关闭连接的一方保持的状态,对于爬虫服务器来说他本身就是“客户端”,在完成一个爬取任务之后,他就会发起主动关闭连接,从而进入TIME_WAIT的状态,然后在保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭回收资源。为什么要这么做?明明就已经主动关闭连接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定的,主要出于以下两个方面的考虑:

        • 防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
        • 可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
      • 解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。修改/etc/sysctl.conf。修改完之后执行/sbin/sysctl -p让参数生效。
    • 保持close_wait的解决:

      • TIME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。
      • 但是CLOSE_WAIT就不一样了,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。
      • 所以问题一定在程序里,检查就完事了
  • 缓冲区溢出攻击原理,buffer 和 cache区别

    • 缓冲区溢出的含义是为缓冲区提供了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。通常情况下,缓冲区溢出的数据只会破坏程序数据,造成意外终止。但是如果有人精心构造溢出数据的内容,那么就有可能获得系统的控制权
    • 缓冲区在系统中的表现形式是多样的,高级语言定义的变量、数组、结构体等在运行时可以说都是保存在缓冲区内的,因此所谓缓冲区可以更抽象地理解为一段可读写的内存区域,缓冲区攻击的最终目的就是希望系统能执行这块可读写内存中已经被蓄意设定好的恶意代码。按照冯·诺依曼存储程序原理,程序代码是作为二进制数据存储在内存的,同样程序的数据也在内存中,因此直接从内存的二进制形式上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出攻击提供了可能。
    • 由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:

      • 淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
      • 淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
      • 淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!
      • 淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。
      • 淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。
  • IO多路复用技术:

    • IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

    • IO多路复用适用如下场合:

      • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
      • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
      • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
      • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
      • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
    • select函数

      • 说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。

      • 该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下 ```c

        include

        include

int select(int maxfdp1,fd_set readset,fd_set writeset,fd_set exceptset,const struct timeval timeout) 返回值:就绪描述符的数目,超时返回0,出错返回-1

  1. -
  2. 函数参数介绍如下
  3. - 第一个参数maxfdp1指定待测试的文件描述字(file description)个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字012...maxfdp1-1均将被测试。因为文件描述符是从0开始的。
  4. - 中间的三个参数readsetwritesetexceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符
  5. - timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
  6. -
  7. select 在一个进程内可以维持最多 1024 个连接
  8. -
  9. select过程如下
  10. - 使用copy_from_user从用户空间拷贝fd_set到内核空间
  11. - 注册回调函数__pollwait
  12. - 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_pollsock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll
  13. - tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  14. - __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
  15. - poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  16. - 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd
  17. - fd_set从内核空间拷贝到用户空间。
  18. -
  19. select缺点
  20. - 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  21. - 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  22. - select支持的文件描述符数量太小了,默认是1024
  23. -
  24. [poll函数](https://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html)
  25. -
  26. poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。pollselect同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核态的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
  27. -
  28. poll函数原型如下
  29. ```c
  30. # include <poll.h>
  31. int poll ( struct pollfd * fds, unsigned int nfds, int timeout);


pollfd结构体定义如下

  1. struct pollfd {
  2. int fd; /* 文件描述符 */
  3. short events; /* 等待的事件 */
  4. short revents; /* 实际发生了的事件 */
  5. } ;


每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。

  1. -

使用poll()和select()不一样,你不需要显式地请求异常情况报告。

  1. -

成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1

  1. -

其实select跟poll是差不多的,复用了很多代码,只是记录监听events的数据结构不一样

  • epoll函数

    • epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    • epoll操作过程需要三个接口,分别如下

      1. #include <sys/epoll.h>
      2. int epoll_create(int size);
      3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
      4. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  1. -

int epoll_create(int size)

  1. - 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  2. -

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

  1. - epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  2. -

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

  1. - 等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
  2. -

epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger,边缘触发)。LT模式是默认模式,LT模式与ET模式的区别如下:

  1. - LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  2. - ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
  3. - ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  4. - epollepoll实例创建、events增删改还有events轮询都分开了,这样的话epoll实例就可以被同一个进程中的所有线程共享。epollpoll一样,使用链表节点记录监听events,但是呢它有三个链表型结构(就绪链表、辅助链表、红黑树),首先想要监听的events的节点被放到红黑树里,这样可以加快events节点的访问。events就绪之后会被挂载到就绪链表里去,当epoll_wait从内核空间向用户空间写出就绪events的时候,会遍历就绪链表,同时这个时候可能还会发生新的就绪events,这个时候已就绪的events不再添加到就绪链表里去,而是使用辅助链表
  5. - epoll_wait调用中,epoll会遍历就绪队列里的每一个events节点,然后通过文件的poll方法再次获取事件的最新状态revents,然后把该events节点从就绪链表中删除。当revents中包含我们关心的事件events的话,LT模式还会把该节点重新加入到就绪队列里,而ET模式也就是edge边界模式不会。
  6. - 这么做有什么影响呢,假设我们监听一个管道可读,当事件就绪之后,我们只读了部分内容,还有部分内容没有读。当我们再次epoll_wait的时候,对LT模式来说,就绪队列里还有这个事件的节点,再次获取状态,然后返回这个这个事件;对ET模式来说,就绪队列里没有这个事件的节点了,所以也就不会再对它进行通知了。那LT模式中的这个事件节点什么时候被删除呢,假设第一次epoll_wait的时候,我们把管道里的内容全部读完了,下次epoll_wait遍历到这个节点然后重新获取它的状态的时候,它已经不再就绪了,因为管道空了,这个时候LT模式就不会再把这个节点重新添加到就绪队列里了。
  7. - LT模式和ET模式各自的应用场所:LT模式比较慢,但是比较安全,也就是如果真的是就绪的话它会再次通知你;而ET模式比较快,但是有可能造成事件的丢失,这就可能让程序永远阻塞。LT为了担责,降低了效率,而ET为了效率将责任推给了用户
  8. -

与select的三个缺点相比:

  1. - 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  2. - 对于第二个缺点,epoll的解决方案不像selectpoll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd,利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的
  3. - 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
  • 总结

    • select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd内存碎片(外碎片和内碎片)集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
    • select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
      • 惊群效应
  • 对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:

    • 系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
    • 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
  • 对于惊群效应的场景描述,最常见的就是对于socket操作符的accept操作的描述。当多个用户进程/线程同时监听同一个端口时,由于实际上一个请求过来,只有一个进程/线程accept成功,所以就会产生惊群效应。
  • linux操作系统在内核层面很早就解决了这个问题,一个请求过来,内核只会唤醒一个进程来accept,这样就没有惊群现象了。但是在很多场景下,只要有竞争,就可能会出现惊群效应。比如常见的生产者-消费者模型,一般来说消费可能会比较耗时,所以消费者会有多个。当突然有生产者往队列里面投了一个job时,这些消费者就会一哄而上去队列中抢这个job,这就发生了惊群效应。
    • 创建一个TCP服务器的步骤是什么
  • 从汇编去解释一下引用(我们先来看看左值引用吧(画图示意),左值引用封装了一个指针,指针指向被引用的对象,每次使用这个引用就是解引用这个被封装的指针。右值引用的话,底层是将原来的对象进行了一份内存拷贝,然后封装了对这个拷贝的指针。因为是拷贝,所以实际上右值引用其实也是左值,emmm…STL里面有一个forkward函数,它的作用就是用来进行右值引用的类型恢复…)

  • 惊群效应了解吗,如何解决惊群呢(我先举个例子吧,linux内核中的等待队列,等待队列中的等待节点有两种状态,一种是互斥等待,一种是非互斥等待。如果某个事件一发生,会唤醒对应的等待队列中的所有非互斥等待节点,而如果是互斥等待节点的话,可以选择唤醒所有节点,也可以选择唤醒指定个节点。Pthread线程库里面也有一个很好的例子,pthread_cond_signal与pthread_cond_broadcast,signal只通知一个信号量,而broadcast会通知所有信号量。但是有时候就绪的事件只能满足一个用户,如果选择广播的话就会通知所有用户,然后最终只有一个用户可以得到满足,其他用户还是被阻塞导致不必要的性能浪费)

  • 闪存介质的写放大问题,如何优化?

  • 计算机的磁盘读取文件的过程