目录

  1. 题目
    1. 操作系统
    2. 进程与线程
  2. 参考答案
    1. 操作系统
    2. 进程与线程

题目

操作系统

  1. 系统中断是什么,用户态和内核态的区别
  2. PCB内存布局
  3. 虚拟内存和物理内存怎么对应
  4. 操作系统中内存块以何种数据结构构成

进程与线程

  1. 用过多线程吗,以前的多线程代码还能怎么优化,线程池的实现。答案
  2. 单核机器上写多线程程序,是否需要考虑加锁,为什么?答案
  3. 线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的。答案
  4. 线程间的同步方式,最好说出具体的系统调用。答案
  5. 进程与线程什么意思,为什么要有进程线程,其中有什么区别,他们各自怎么同步的。答案
  6. 多进程和多线程的使用场景。答案
  7. 死锁,如何解决死锁?答案
  8. 进程间通讯方式。答案
  9. 怎么保证一个CPU只有一个线程运行?
  10. 线程的基本组成是什么?答案
  11. 线程有什么状态?运行挂起结束状态,有新生状态吗?答案
  12. 守护进程、僵尸进程、孤儿进程,守护进程的作用是什么?答案
  13. 协程是什么?答案
  14. 进程的状态?答案

参考答案

操作系统

进程与线程

    • 为何需要线程池:频繁的创建和销毁线程会大大地浪费时间和效率,更浪费内存。线程池便是针对这一问题而设计的。可以很好地解决线程的重复利用,避免重复开销。
    • 线程池的优点:
      • 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用;
      • 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
    • 线程池的风险:
      • 死锁
      • 资源不足:除了线程对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。
      • 并发错误:线程保存空闲状态,尽管队列中有工作要处理;
      • 线程泄漏:当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生线程泄漏。
      • 请求过载:仅仅是请求就压垮了服务器。解决:在某些情况下,可以简单地抛弃请求,依靠更高级别的协议稍后重试请求;也可以用一个指出服务器暂时很忙的响应来拒绝请求。
        返回原题
    • 单核CPU跑多线程程序,也需要加线程锁。理由如下:
      • 时间片的大小不定:比如单个时间片只能完成给定任务的一半,剩下的那一半就是只有等下一个时间片了。而在等待的过程中,如果不加锁,就没法保证其他线程不使用我们的资源,数据就有可能出错;
      • 给线程分配时间片的时机不确定:比如有a、b、c三个进程,a、c使用了共享的资源。如果前十个时间片都集中在a、b上,那么很有可能时间片分给c的时候,a已经完成了工作,因此它也不需要和c争夺资源了。如果a、c的优先级比b高,那么时间片可能大部分分配在a、c上,这个时候a、c就要疯狂争夺资源了。
        返回原题
    • 线程切换时需要保存当前线程ID、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP、PC、EAX等寄存器。
    • SP(Stack Pointer):堆栈指针,指向当前栈的栈顶地址;
    • PC(Program Counter):程序计数器,存储下一条将要执行的指令;
    • EAX(Extended Accumulator Register):累加寄存器,用于加法乘法的缺省寄存器。
      返回原题
    • 互斥锁(mutex):用来保证同一时间内只有一个线程在执行某段代码(临界区)。在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。主要包含以下基本函数:
      • 互斥锁初始化:pthread_mutex_init();
      • 互斥锁上锁:pthread_mutex_lock();
      • 互斥锁判断上锁:pthread_mutex_trylock();
      • 互斥锁解锁:pthread_mutex_unlock();
      • 消除互斥锁:pthread_mutex_destroy()。
    • 信号量(semaphore):和mutex类似,表示可用资源数量,不同的是这个数量可以大于1。步骤为 信号量初始化->等待信号量->释放信号量->销毁信号量;
    • 条件变量(cond):与互斥锁结合使用,是用来等待而非上锁的。它用于阻塞线程等待某个事件的发生,并且当等待的事件发生时,阻塞线程会被通知。步骤为:初始化条件变量->等待条件成立->激活条件变量->清除条件变量。
    • 读写锁(reader-writer lock):也称共享互斥锁。有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只能有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁。因此与互斥量相比,读写锁允许有更高的并行性。
    • 临界区(critical section):主要用于同步控制;
    • 临界区和互斥体的区别:
      • 临界区只能用于本进程内的线程,而不可用来同步多个进程中的线程;互斥量、信号量、事件都可以跨越进程使用来进行同步数据操作;
      • 临界区是非内核对象,只在用户态进行锁操作,速度快;互斥体是内核对象,在核心态进行锁操作,速度慢。
        返回原题
    • 进程和线程的区别:
      • 进程是资源分配的最小单位,线程是CPU调度的最小单位。
      • 进程有独立的地址空间,在保护模式下,一个进程崩溃后不会影响其他进程。而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但没有独立的地址空间,一个线程崩溃就等于整个进程崩溃。所以多进程的程序要比多线程的程序更加健壮。
      • 进程切换时,耗费资源大,效率要比线程低;
      • 一个程序至少要有一个进程,一个进程至少要有一个线程;
      • 线程的划分尺度小于进程,因此多线程程序的并发性更高;
      • 由于多个线程共享内存单元,从而极大地提高了程序的运行效率;
      • 在执行过程中,每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
    • 多进程同步(以下是Linux系统的,Windows系统相比少了一个信号):
      • 管道(pipe):用于父子进程间的通信;
      • 信号(signal):进程间传递信号,捕获到信号后执行对应绑定的代码,和QT的信号槽类似。
      • 信号量(semaphore):本身无法传递数据,配合共享内存使用,类似于线程中的锁,用于保护临界资源;
      • 共享内存(shared memory):进程间最常见的数据同步方式,与信号量配合使用;
      • 消息队列(message):把数据放入队列,内核逐一处理发送至目的线程;
      • 套接口(socket):可用于不同机器之间的进程间通信
    • 多线程同步:参考上题  
      返回原题
    • 多进程的优点:
      • 编程相对容易,无需考虑锁和同步资源的问题;
      • 更强的容错性:一个进程崩溃了不会影响其他进程;
      • 有内核保证的隔离:数据和错误隔离;
    • 多进程案例:
      • Chrome浏览器、redis、nginx;
    • 多线程的优点:
      • 创建速度快;
      • 数据共享更加高效:多线程共享同一虚拟地址空间;
      • 轻松的上下文切换开销:不用切换地址空间、不用更改寄存器、不用刷新TLB;
      • 提供非均质的服务:如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。
    • 多线程案例:
      • 桌面软件:响应用户输入的是一个线程,后台程序处理是另外的线程
        返回原题
    • 死锁:进程死锁的简称。是指两个或两个以上的进程在执行过程中,以至于竞争资源或由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
    • 造成死锁的必要条件:
      • 互斥条件:一个资源每次只能被一个线程使用;
      • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
      • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺;
      • 循环等待条件:若干线程之间形成一种首尾相接的循环等待资源关系。
    • 避免死锁:破坏产生死锁的4个必要条件之一便可以避免死锁。
      返回原题
    • Linux系统进程间的通信方式:管道(pipe)、信号(signal)、消息队列(Message)、共享内存(shared memory)、信号量(semaphore)、套接字(socket)。
    • Windows系统进程间的通信方式:管道(pipe)、信号(signal)、互斥量(mutex)、共享内存(shared memory)、套接字(socket)。
      返回原题
    • 线程ID:ID在本进程中是唯一的,用于标识线程;
    • 当前指令指针(PC)
    • 寄存器集合:由于线程是并发执行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有线程的寄存器集合的状态进行保存,以便将来该线程在被重新切换时能得以恢复;
    • 堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。一个进程的线程共享堆区。
      返回原题
    • 状态:
      • 新建(New):创建后尚未启动的线程状态;
      • 运行(Runable):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间;
      • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒;
      • 限期等待(Time Waiting):处于这种状态的线程不会被分配CPU执行时间,不会必须被其他线程显式地唤醒,在一定时间后它们会由操作系统自动唤醒;
      • 阻塞(Blocked):线程被阻塞了,等待着获取一个排它锁,这个事件将在另一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间,或者唤醒的发生,在程序等待进入同步区域时,线程将进入这种状态;
      • 线束(Terminated):已终止的线程状态,线程已经结束执行。
        返回原题
    • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。所以孤儿进程不会对系统造成危害;
    • 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称为僵尸进程。
      • 危害:如果大量地产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
      • 解决:将父进程杀死,所有僵尸进程变为孤儿进程,被init收养,然后被释放资源;父进程对子进程进行wait()或waitpid()调用来释放其所占有资源。
    • 守护进程:是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
      • 常见的守护进程:日志进程syslogd、web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
      • 特点:一个守护进程的父进程是init进程,命名通常以d结尾;一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行;守护进程通常以root权限运行。
        返回原题
    • 协程(Coroutines):是一种比线程更加轻量级的存在。一个线程可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制,即在用户态执行。
    • 使用协程的好处:性能得到了很大提升,不会像线程切换那样消耗资源。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。
    • Python:可以通过yield/send的方式实现协程。python3.5后,async/await成为了更好的替代方案。
      返回原题
    • 参考:进程状态(含状态变迁图)
    • 三态模型:
      • 就绪:当一个进程获得了除处理器以外的一切所需资源,一旦得到处理器即可运行,则称此进程处理就绪状态。
      • 运行:当一个进程正在处理器上运行时,则称该进程处于运行状态。
      • 阻塞:也称等待或睡眠状态,一个进程正在等待某一事件发生,而暂时停止运行。这时即使把处理器分配给进程也无法运行,故称该进程处于阻塞状态。
    • 五态模型:新增新建态和终止态
      • 新建态:对于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。进程正在创建过程中,还不能运行。
      • 终止态:进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息。
        返回原题