面试题12

86 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 说说多线程和多进程的不同

(1)一个线程从属于一个进程;一个进程可以包含多个线程。

(2)一个线程挂掉,对应的进程挂掉,多线程也会挂掉;一个进程挂掉不会影响其他进程,多进程稳定。

(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。

(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。

(5)多进程切换时需要TLB获取新的地址空间,然后切换硬件上下文和内核栈;而多线程切换时只需要切换硬件上下文和内核栈。

(6)通信方式不一样。多线程的通信方式有临界区、互斥量、信号量、条件变量、读写锁;多进程的通信方式有管道、系统PIC(包括消息队列、信号量、信号、共享内存)、套接字socket。

(7)多进程适应于多核、多机分布;多线程适用于多核。

2. 简述互斥锁的机制,互斥锁与读写锁的区别

互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

互斥锁与读写锁: (1)读写锁区分读者和写者,而互斥锁不区分;

(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

互斥锁原理:

互斥锁其实就是一个bool型变量,为true时表示锁可获取,为false时表示已上锁。这里的互斥锁其实是泛指Linux所有的锁机制。

我们采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个线程在进入临界区时应得到锁;它在退出临界区时释放锁。函数acquire()获取锁,而函数release()释放锁,如下面的伪代码:

do {
	获得锁
	关键区
	释放锁
	剩余区
} while(true);

每个互斥锁都有一个布尔变量available,它的值表示锁是否可用。如果锁可用,那么调用acquire()会成功,并且锁不再可用。当一个线程试图获取不可用的锁时,它会阻塞,知道锁被释放。

如下定义acquire():

acquire() {
	while (!available);
	
	/* busy wait */
	available = false;
}

如下定义release():

release() {
	available = true;
}

3. 说说什么是信号量,有什么作用?

概念

信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只能由一个进程独享。

原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:

(1)P(sv)操作

如果sv的值大于0,就给它减1;如果它的值为0,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。

(2)V(sv)操作

如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态知道信号量的值大于0,若进程被唤醒则返回第一步)。

作用

用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

4. 进程、线程的中断切换过程是怎么样的?

上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。

进程上下文切换

(1)保护被中断进程的处理器的现场信息; (2)修改被中断进程的进程控制块有关信息,如进程状态等; (3)把被中断进程的进程控制块加入有关队列; (4)选择下一个占有处理器运行的进程; (5)根据被选中进程设置操作系统用到的地址转换和存储保护信息;

切换页目录以使用新的地址空间(TLB) 切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)

(6)根据被选中进程恢复处理器现场

线程上下文切换

(1)保护被中断线程的处理器的现场信息; (2)修改被中断线程的线程控制块有关信息,如线程状态等; (3)把被中断线程的线程控制块加入有关队列; (4)选择下一个占有处理器运行的线程; (5)根据被选中线程设置操作系统用到的存储保护信息;

切换内核栈和硬件上下文(切换堆栈,以及各寄存器)

(6)根据被选中线程恢复处理器现场。

5. 简述自旋锁和互斥锁的使用场景

(1)互斥锁用于临界区持锁时间比较长的操作,比如下面这种情况都可以考虑 a.临界区有IO操作 b.临界区代码复杂或者循环量大 c.临界区竞争非常激烈 d.单核处理器

(2)自旋锁就主要用在持锁时间非常短且CPU资源不紧张的情况下。

6. 多线程和单线程有什么区别,多线程编程需要注意什么,多线程加锁需要注意什么?

区别

(1)多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂掉。

(2)一个进程里有多个线程,可以执行多个任务;一个进程里只有一个线程就只能执行一个任务。

(3)多线程并发执行多任务,需要切换内核栈与硬件上下文,有切换的开销;单线程不需要切换,没有切换的开销。

(4)多线程并发执行多任务,需要考虑同步的问题;单线程则不用考虑。

多线程编程需要考虑的问题

多线程编程需要考虑同步的问题。线程间的同步方式包括互斥锁、信号量、条件变量、读写锁。

多线程加锁需要注意什么

多线程加锁需要注意死锁的问题。破坏死锁的必要条件来避免死锁。

7. 说说sleep和wait的区别

sleep

sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。

在linux下,sleep函数的参数是毫秒 例如:

#include <windows.h>// 首先应该先导入头文件 

//注意第一个字母是大写。 //就是到这里停半秒,然后继续向下执行。
Sleep (500) ; 

在linuxC语言中sleep的单位是秒 例如:

#include <unistd.h>// 首先应该先导入头文件
 
//停5秒 //就是到这里停5秒,然后继续向下执行。
sleep(5);

wait

wait是父进程回收子进程PCB资源的一个系统调用。进程一旦调用了wait函数,就立即阻塞本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程时,wait就会手机这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。

函数原型:

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);  

子进程的结束状态值会由参数statues返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,参数status可以设成NULL。

区别:

(1)sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。

(2)wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用。

8. 说说线程池的设计思路,线程池中线程的数量由什么来确定

设计思路:

(1)设置一个生产者消费者队列,作为临界资源;

(2)初始化n个线程,并让它们运行起来,加锁区队列里取任务运行;

(3)当任务队列为空时,所有线程阻塞。

(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

线程池中线程数量:

线程数量和哪些因素有关:CPU、IO、并行、并发 如果是CPU密集型应用,则线程池大小设置为:CPU数目 + 1;

如果是IO密集型应用,则线程池大小设置为:2*CPU数目 + 1;

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

所以线程等待时间比越高,线程需要越多;线程CPU时间所占比例越高,线程需要越少。

为什么要创建线程池:

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务所花的时间还要多,频繁的创建线程和销毁线程,再加上业务工作流程,消耗系统资源的时间,可能会导致系统资源不足。同时线程池也是为了提升系统效率而被创造出来的。

线程池的核心线程和普通线程:

任务队列可以存放100个任务,此时为空,线程池里有10个核心线程,若突然来了10个任务,那么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创建20个普通线程来处理多余的任务。 以上是线程池的工作流程。

9. 进程和线程相比,为什么慢?

(1)进程系统开销比线程大;线程所需要的系统资源更少。 (2)进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换上下文和内核栈。 (3)进程通信比线程通信所花费的开销大。进程通信需要借助管道、消息队列、共享内存、需要额外申请空间,通信繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。

10. 简述Linux零拷贝的原理

什么是零拷贝:

所谓【零拷贝】,描述的是计算机操作系统中,CPU不执行将数据从一个内存区域,拷贝到另一个内存区域的任务。通过网络传输文件时,可以节省CPU周期和内存带宽。

零拷贝的好处:

(1)节省了CPU周期,空出的CPU可以完成更多其他的任务; (2)减少了内存区域之间数据的拷贝,节省内存带宽; (3)减少用户态和内核态之间数据的拷贝,提升数据传输效率; (4)应用零拷贝技术,减少用户态和内核态之间的上下文切换。

零拷贝原理:

在传统的IO中,用户态与内核态空间之间的复制时完全不必要的,因为用户态空间仅仅只起到了一种数据转存媒介的作用,除此之外,没有做任何事情。

(1)Linux 提供了 sendfile() 用来减少我们的数据拷贝和上下文切换次数。 在这里插入图片描述 a. 发起 sendfile() 系统调用,操作系统由用户态空间切换到内核态空间(第一次上下文切换)

b. 通过 DMA 引擎将数据从磁盘拷贝到内核态空间的输入的 socket 缓冲区中(第一次拷贝)

c. 将数据从内核空间拷贝到与之关联的 socket 缓冲区(第二次拷贝)

d. 将 socket 缓冲区的数据拷贝到协议引擎中(第三次拷贝)

e. sendfile() 系统调用结束,操作系统由用户态空间切换到内核态空间(第二次上下文切换)

根据以上过程,一共有 2 次的上下文切换,3 次的 I/O 拷贝。我们看到从用户空间到内核空间并没有出现数据拷贝,从操作系统角度来看,这个就是零拷贝。内核空间出现了复制的原因: 通常的硬件在通过DMA访问时期望的是连续的内存空间。 (2)mmap数据零拷贝原理 如果需要对数据做操作,Linux提供了mmap的零拷贝来实现。