驱动工程师面试题 - 操作系统1

驱动工程师面试题 - 操作系统1

本文档采用口语化的面试回答风格,模拟真实面试场景中的思路和表达方式


1 进程与线程

1.1 进程与线程有什么区别?

回答思路:

进程和线程都是操作系统的执行单元,但它们有本质区别。

资源分配

进程是资源分配的基本单位,每个进程有独立的地址空间、代码段、数据段、堆栈。线程是CPU调度的基本单位,同一进程内的线程共享进程的地址空间和资源。

开销

创建进程的开销大,因为要分配独立的地址空间,拷贝父进程的资源。创建线程的开销小,只需要分配栈空间和线程控制块。

进程切换开销大,要切换地址空间,刷新TLB和Cache。线程切换开销小,因为共享地址空间,只需要切换寄存器和栈。

通信方式

进程间通信需要特殊机制,像管道、消息队列、共享内存,因为地址空间隔离。线程间通信简单,直接访问共享变量就行,因为共享地址空间。

独立性

进程之间相互独立,一个进程崩溃不影响其他进程。线程之间不独立,一个线程崩溃可能导致整个进程崩溃。


1.2 进程有哪些状态?状态之间如何转换?

回答思路:

进程最基本的是三态模型:就绪、运行、阻塞。

就绪状态(Ready)

进程已经准备好运行,所有需要的资源都有了,就等CPU分配。多个就绪进程在就绪队列里排队。

运行状态(Running)

进程正在CPU上执行指令。单核系统同一时刻只有一个进程在运行状态。

阻塞状态(Blocked/Waiting)

进程等待某个事件,比如I/O完成、信号量释放、等待锁,暂时不能运行。即使给它CPU也没用,因为条件不满足。

三态转换

就绪到运行:调度器选中这个进程,分配CPU。

运行到就绪:时间片用完,或者被更高优先级进程抢占。进程还想运行,但CPU被拿走了。

运行到阻塞:进程主动等待事件,比如读文件、等待锁。进程调用阻塞操作,主动放弃CPU。

阻塞到就绪:等待的事件发生了,比如I/O完成、锁被释放,进程重新进入就绪队列,等待CPU分配。

注意:阻塞状态不能直接到运行状态,必须先到就绪状态,再由调度器分配CPU。

扩展的五态模型

实际操作系统还有新建状态(进程刚创建,还没加入就绪队列)和终止状态(进程执行完毕或被杀死)。

但核心还是三态模型,理解了三态转换,就理解了进程调度的本质。


1.3 进程间通信有哪些方式?各有什么特点?

回答思路:

进程间通信有很多种方式,常用的有这几种:

管道(Pipe)

分为匿名管道和命名管道。匿名管道只能用于有亲缘关系的进程,命名管道可以用于任意进程。

特点是简单,但只能单向通信,而且容量有限。适合简单的数据传递。

消息队列

进程可以往队列里发消息,其他进程从队列里取消息。

特点是可以实现异步通信,消息有类型,可以选择性接收。但消息大小有限制,而且要拷贝数据。

共享内存

多个进程映射同一块物理内存,直接读写。

特点是速度最快,因为不需要拷贝数据。但需要自己处理同步问题,要配合信号量或互斥锁使用。

信号量

主要用于进程间同步,控制对共享资源的访问。

特点是轻量级,但只能传递简单的同步信号,不能传递数据。

信号(Signal)

用于通知进程某个事件发生了,比如SIGKILL、SIGTERM。

特点是异步,但只能传递简单的通知,不能传递复杂数据。

Socket

可以用于本机进程间通信,也可以用于网络通信。

特点是通用性强,但开销比其他方式大。


1.4 线程同步与互斥有哪些机制?各适用什么场景?

回答思路:

线程同步和互斥是并发编程的两个核心概念。互斥是保护临界区,防止多个线程同时访问共享资源。同步是协调线程执行顺序,让线程按特定顺序执行。

互斥机制

互斥锁(Mutex)

最基本的互斥机制,保证同一时间只有一个线程能进入临界区。

适用场景:保护共享数据,临界区执行时间较长的情况。如果拿不到锁,线程会睡眠,不占用CPU。

自旋锁(Spinlock)

拿不到锁时,线程会一直循环等待,不睡眠。

适用场景:临界区执行时间很短,或者在中断上下文中,因为中断上下文不能睡眠。

读写锁(RWLock)

允许多个线程同时读,但写的时候独占。

适用场景:读多写少的情况,可以提高并发性能。

同步机制

条件变量(Condition Variable)

配合互斥锁使用,让线程等待某个条件满足。

适用场景:生产者-消费者模型,线程需要等待某个条件才能继续执行。

信号量(Semaphore)

可以控制多个线程同时访问资源,也可以用于线程间同步。

适用场景:限制同时访问资源的线程数量(资源计数),或者线程间的执行顺序控制(同步信号)。


1.5 什么是上下文切换?开销在哪里?

回答思路:

上下文切换就是CPU从一个进程或线程切换到另一个,需要保存当前的执行状态,恢复下一个的执行状态。

切换过程

第一步,保存当前进程的上下文,包括寄存器、程序计数器、栈指针等,保存到进程控制块(PCB)。

第二步,选择下一个要运行的进程,这是调度器的工作。

第三步,恢复下一个进程的上下文,从PCB里加载寄存器、程序计数器、栈指针等。

第四步,如果是进程切换,还要切换地址空间,刷新TLB和Cache。

开销在哪里

第一,保存和恢复寄存器的开销。

第二,切换地址空间的开销,要刷新TLB,导致后续的内存访问会有TLB miss。

第三,Cache失效的开销。新进程的数据不在Cache里,会有很多Cache miss,性能下降。

第四,调度器选择下一个进程的开销。

如何减少上下文切换

第一,减少线程数量,避免过度并发。

第二,使用线程池,避免频繁创建销毁线程。

第三,减少锁的竞争,降低线程阻塞的概率。

第四,使用无锁数据结构,避免线程因为等锁而切换。


2 用户态与内核态

2.1 什么是用户态和内核态?为什么要区分?

回答思路:

用户态和内核态是CPU的两种运行模式,也叫特权级。

用户态

CPU运行在低特权级,只能执行普通指令,不能直接访问硬件,不能执行特权指令。应用程序运行在用户态。

内核态

CPU运行在高特权级,可以执行所有指令,可以直接访问硬件,可以访问所有内存。操作系统内核运行在内核态。

为什么要区分

核心目的是保护

通过特权级隔离,用户程序不能直接操作硬件、不能访问内核内存、不能执行特权指令。所有对关键资源的访问都必须通过内核,内核可以检查权限、验证参数。

这样一个应用程序出错或者有恶意行为,只会影响自己,不会破坏内核或其他程序,保证了系统的稳定和安全。

如果不区分特权级,任何程序都能直接操作硬件和内存,一个程序的bug就可能导致整个系统崩溃。


2.2 用户态如何切换到内核态?有哪些方式?

回答思路:

用户态切换到内核态有三种主要方式:

系统调用

这是最常见的方式。应用程序需要内核服务时,比如读文件、创建进程,就调用系统调用。系统调用会触发一个软中断或者陷阱指令,CPU切换到内核态,执行内核代码。

比如应用调用read函数,最终会执行系统调用,进入内核,内核驱动读取数据,再返回用户态。

硬件中断

外部硬件设备产生中断时,CPU会自动切换到内核态,执行中断处理函数。中断是异步的,随时可能发生。

比如网卡收到数据包,产生中断,CPU切换到内核态,执行网卡驱动的中断处理函数。

异常(Exception)

程序执行出错时,CPU内部产生异常,切换到内核态,执行异常处理函数。异常是同步的,由当前指令引起。

比如程序访问空指针,产生缺页异常或段错误,CPU切换到内核态,内核发送SIGSEGV信号给进程。

这三种方式都是硬件机制,保证了用户态不能随意进入内核态,只能通过这些受控的方式。


2.3 系统调用的过程是怎样的?

回答思路:

系统调用是用户程序请求内核服务的过程,大概分这几步:

第一步,应用程序调用库函数

比如应用调用read函数,这个read是C库提供的封装函数。

第二步,库函数准备参数

把系统调用号和参数放到特定的寄存器里。比如ARM上,系统调用号放在r7,参数放在r0-r6寄存器。

第三步,触发软中断指令

执行svc指令(Supervisor Call,以前叫swi)。这会触发CPU从用户态切换到内核态。

第四步,CPU切换到内核态

保存用户态的寄存器到栈上,切换到内核栈,跳转到系统调用处理函数。

第五步,内核执行系统调用

根据系统调用号,找到对应的内核函数,执行。比如read系统调用,会调用内核的sys_read函数,再调用具体的驱动函数。

第六步,返回用户态

内核函数执行完毕,把返回值放到r0寄存器,恢复用户态的寄存器,切换回用户态,继续执行应用程序。

整个过程涉及两次特权级切换,有一定开销,所以频繁的系统调用会影响性能。


2.4 用户态和内核态切换的开销是什么?

回答思路:

用户态和内核态切换的开销主要在这几个方面:

保存和恢复寄存器

切换时要保存用户态的所有寄存器,切换回来时要恢复。寄存器很多,这个操作有开销。

切换栈

用户态有用户栈,内核态有内核栈,切换时要切换栈指针。

TLB和Cache的影响

切换到内核态,访问的是内核地址空间,用户态的TLB和Cache可能失效。切换回用户态,又要重新加载用户态的数据到Cache,会有Cache miss。

权限检查

内核要检查系统调用的参数是否合法,比如指针是否指向用户空间,这也有开销。

上下文切换的时间

整个切换过程需要几百到几千个CPU周期,虽然看起来不多,但如果频繁切换,累积起来开销就大了。

如何减少开销

第一,减少系统调用次数。比如批量读写,一次读写多个数据,而不是每次读写一个。

第二,使用mmap,把文件映射到内存,直接访问内存,避免read/write系统调用。

第三,使用用户态驱动,比如DPDK,绕过内核,直接在用户态访问硬件,避免内核态切换。


3 并发控制

3.1 什么是临界区和临界资源?

回答思路:

临界区和临界资源是并发编程中的核心概念。

临界资源

一次只能被一个进程或线程使用的资源,就叫临界资源。比如共享变量/缓冲区、硬件寄存器、链表节点。

如果多个线程同时访问临界资源,可能导致数据不一致。比如两个线程同时对一个变量加1,结果可能只加了1次,不是2次。

临界区

访问临界资源的那段代码,就叫临界区。

比如对共享变量count加1的代码,就是临界区。临界区必须互斥执行,同一时间只能有一个进程在临界区里。

临界区的要求

第一,互斥访问。同一时间只有一个进程能进入临界区。

第二,有限等待。进程请求进入临界区,不能无限等待,要在有限时间内进入。

第三,空闲让进。如果没有进程在临界区,想进入的进程应该能立即进入。

第四,让权等待。进程等待进入临界区时,应该释放CPU,不要忙等。


3.2 为什么会出现死锁?如何预防?

回答思路:

死锁就是多个线程互相等待对方持有的资源,谁也无法继续执行。

常见的死锁场景

第一,交叉持有锁。线程A持有锁1等待锁2,线程B持有锁2等待锁1,两个线程互相等待,形成死锁。

比如驱动里,线程A先锁设备A再锁设备B,线程B先锁设备B再锁设备A,就可能死锁。

第二,持有锁等待事件。线程持有锁,然后等待某个事件,但这个事件需要其他线程获取同一个锁才能触发。

比如线程A持有锁,等待条件变量,但线程B要获取这个锁才能修改条件,结果死锁。

第三,资源申请顺序不一致。多个线程以不同顺序申请多个资源,可能形成循环等待。

如何预防死锁

第一,统一加锁顺序。给所有锁定义一个全局顺序,所有线程都按这个顺序加锁。这样就不会形成循环等待。

比如规定先锁设备A再锁设备B,所有线程都遵守这个顺序,就不会死锁。

第二,使用trylock。尝试获取锁,如果失败就释放已持有的锁,避免持有并等待。

第三,避免嵌套锁。尽量一次只持有一个锁,减少死锁可能。

第四,设置超时。等待锁时设置超时,超时就放弃,避免无限等待。


3.3 互斥锁与自旋锁有什么区别?各适用什么场景?

回答思路:

互斥锁和自旋锁都是用来保护临界区的,但实现方式不同。

互斥锁(Mutex)

拿不到锁时,线程会睡眠,进入阻塞状态,释放CPU。等锁被释放时,线程被唤醒,重新竞争锁。

优点是不占用CPU,适合临界区执行时间较长的情况。

缺点是睡眠和唤醒有开销,涉及上下文切换。

自旋锁(Spinlock)

拿不到锁时,线程会一直循环检查锁是否被释放,不睡眠,一直占用CPU。

优点是没有上下文切换的开销,如果临界区很短,很快就能拿到锁。

缺点是一直占用CPU,如果临界区执行时间长,会浪费CPU资源。

使用场景

互斥锁适用于:临界区执行时间较长,或者在用户态程序中。

自旋锁适用于:临界区执行时间很短(几十个指令),或者在中断上下文中(因为中断上下文不能睡眠)。

驱动开发中的选择

内核驱动里,如果临界区很短,用自旋锁。如果临界区可能睡眠,或者执行时间长,用互斥锁。

中断处理函数里,只能用自旋锁,因为中断上下文不能睡眠。


3.4 信号量的P/V操作是什么?如何使用?

回答思路:

信号量是一个整数变量,用来控制对共享资源的访问。

P操作(wait操作)

信号量减1,如果结果小于0,进程阻塞等待。如果结果大于等于0,进程继续执行。

P操作表示申请资源,如果资源不够,就等待。

V操作(signal操作)

信号量加1,如果有进程在等待,唤醒一个进程。

V操作表示释放资源,如果有进程在等待,就唤醒它。

信号量的类型

二值信号量:只有0和1两个值,相当于互斥锁。

计数信号量:可以有多个值,用来控制多个资源。

使用场景

第一,互斥访问。用二值信号量保护临界区,初始值为1。进入临界区前P操作,出来后V操作。

第二,资源计数。比如驱动接收数据帧,用计数信号量记录待处理的帧数,初始值为0。驱动每收到一帧数据V操作,进程每处理一帧数据P操作。信号量的值就表示当前有多少帧数据待处理。


3.5 读写锁是什么?什么时候用?

回答思路:

读写锁是一种特殊的锁,允许多个线程同时读,但写的时候独占。

读锁(共享锁)

多个线程可以同时持有读锁,只要没有线程持有写锁。

写锁(排他锁)

只有一个线程可以持有写锁,而且持有写锁时,不能有其他线程持有读锁或写锁。

规则

读-读不互斥,可以并发。

读-写互斥,不能并发。

写-写互斥,不能并发。

使用场景

读多写少的情况。比如配置数据,大部分时间是读取,偶尔才修改。用读写锁可以提高并发性能,多个线程可以同时读取。

如果用普通互斥锁,即使都是读操作,也只能一个一个来,性能就差了。

注意事项

第一,写操作可能饥饿。如果一直有读操作,写操作可能一直拿不到锁。有些实现会给写操作更高优先级。

第二,如果写操作很频繁,读写锁的开销可能比普通互斥锁还大,因为读写锁的实现更复杂。


4 中断处理

4.1 中断的基本概念是什么?中断处理流程是怎样的?

回答思路:

中断是硬件通知CPU有事件发生的机制,让CPU暂停当前工作,去处理这个事件。

中断的基本概念

中断是异步的,CPU不知道什么时候会发生。比如网卡收到数据包,就产生中断,通知CPU处理。

中断有中断号,不同的硬件设备有不同的中断号。CPU根据中断号找到对应的中断处理函数。

中断处理流程

第一步,硬件产生中断信号,发送给中断控制器。

第二步,中断控制器判断中断优先级,如果优先级够高,就通知CPU。

第三步,CPU保存当前的执行状态,包括程序计数器、寄存器、标志位等,保存到栈上。

第四步,CPU关闭中断(或者只关闭同级和低级中断),防止中断嵌套。

第五步,CPU根据中断号,查中断向量表,找到中断处理函数的地址。

第六步,跳转到中断处理函数,执行中断处理代码。

第七步,中断处理完毕,恢复之前保存的执行状态,开启中断。

第八步,CPU继续执行被中断的程序。

整个过程是硬件和软件配合完成的,硬件负责保存状态、查表、跳转,软件负责具体的中断处理逻辑。


4.2 中断上下文与进程上下文有什么区别?

回答思路:

中断上下文和进程上下文是内核的两种执行环境,核心区别是内核是否代表某个特定进程执行。

进程上下文

内核代码代表某个特定进程执行,知道当前是哪个进程。比如进程调用系统调用,进入内核,内核知道是哪个进程发起的调用,这就是进程上下文。

进程上下文可以睡眠,可以被调度,可以访问用户空间(通过copy_from_user等函数)。

中断上下文

内核代码响应中断,不代表任何特定进程。中断随时可能发生,内核不知道当前是哪个进程在运行。

中断上下文不能睡眠,不能被调度,不能访问用户空间。

主要区别

第一,中断上下文不能睡眠。因为中断不属于任何进程,没有进程上下文可以切换。如果中断处理函数睡眠了,就没法唤醒,系统会卡死。

第二,中断上下文不能访问用户空间。因为中断发生时,不知道是哪个进程在运行,用户空间的地址可能无效。

第三,中断上下文要尽快执行完毕。因为中断期间,可能关闭了中断,其他中断无法响应,影响系统实时性。

编程限制

中断处理函数里,不能调用可能睡眠的函数,比如mutex_lock、kmalloc(GFP_KERNEL)、copy_from_user。

只能用不会睡眠的函数,比如spin_lock、kmalloc(GFP_ATOMIC)。


4.3 什么是中断嵌套?会有什么问题?如何避免?

回答思路:

中断嵌套就是在处理一个中断时,又来了另一个中断,打断当前的中断处理。

中断嵌套的问题

第一,增加复杂度。中断处理函数可能被自己打断,要考虑重入问题。

第二,共享数据风险。如果中断处理函数访问共享数据,嵌套可能导致数据不一致。

第三,栈溢出风险。每次中断嵌套都要保存上下文到栈上,嵌套太深可能栈溢出。

如何避免中断嵌套

第一,关闭中断。进入中断处理函数时,关闭当前CPU的中断,这样就不会被其他中断打断。Linux默认就是这样做的。

第二,使用中断下半部。把耗时的工作放到下半部(tasklet、workqueue),上半部只做最紧急的事,快速返回。这样中断处理时间短,即使关中断也不影响系统。

第三,使用自旋锁保护。如果必须允许中断嵌套,访问共享数据时用spin_lock_irqsave保护,它会关闭中断并加锁,保证数据安全。


4.4 什么是中断的上半部和下半部?为什么要分开?

回答思路:

中断处理分为上半部和下半部,是为了平衡实时性和效率。

上半部(Top Half)

就是中断处理函数本身,响应中断时立即执行。

上半部要尽快执行完毕,只做最紧急的工作,比如读取硬件状态、清除中断标志、把数据放到缓冲区。

上半部运行在中断上下文,不能睡眠,要关闭中断或者禁止抢占。

下半部(Bottom Half)

把耗时的工作推迟到下半部执行,比如数据处理、协议栈处理、唤醒进程。

下半部运行在进程上下文或软中断上下文,可以睡眠(如果用workqueue),可以被调度。

为什么要分开

第一,提高实时性。如果中断处理函数执行时间长,其他中断就无法及时响应,影响系统实时性。分开后,上半部很快执行完,其他中断可以及时响应。

第二,避免关中断时间过长。上半部执行时,通常要关闭中断,如果时间长,会影响其他中断。

第三,提高并发性。下半部可以在多个CPU上并发执行,提高效率。

Linux的下半部机制

Softirq(软中断):优先级最高,但使用复杂,一般内核子系统用。

Tasklet:基于softirq实现,使用简单,驱动常用。

Workqueue(工作队列):运行在进程上下文,可以睡眠,适合耗时操作。


4.5 中断处理函数有什么限制?

回答思路:

中断处理函数运行在中断上下文,有很多限制。

不能睡眠

这是最重要的限制。不能调用任何可能睡眠的函数,比如mutex_lock、semaphore、wait_event、kmalloc(GFP_KERNEL)、copy_from_user。

因为中断上下文不属于任何进程,睡眠了就没法唤醒。

不能访问用户空间

不能调用copy_from_user、copy_to_user,因为中断发生时,不知道是哪个进程在运行,用户空间地址可能无效。

要尽快执行完毕

中断处理函数要尽量短,只做必要的工作。耗时的工作要放到下半部。

因为中断处理期间,可能关闭了中断,影响其他中断响应。

不能被调度

中断处理函数不能主动放弃CPU,不能调用schedule。

要注意重入问题

如果允许中断嵌套,中断处理函数可能被自己打断,要保护共享数据。


5 内存管理

5.1 什么是虚拟内存?为什么需要虚拟内存?

回答思路:

虚拟内存是操作系统提供的一种内存管理机制,让每个进程都有独立的地址空间。

什么是虚拟内存

虚拟内存是指CPU访问的地址都是虚拟地址,不是物理地址。这些虚拟地址经过MMU(内存管理单元)查页表转换后,才变成物理地址去访问实际的物理内存。

每个进程都有独立的虚拟地址空间和页表。比如进程A和进程B都可以有地址0x1000,但通过各自的页表映射到不同的物理地址,互不干扰。

为什么需要虚拟内存

第一,进程隔离。每个进程有独立的页表,进程切换时也切换页表。这样每个进程只能访问自己页表映射的物理内存,无法访问其他进程的物理内存,实现了进程隔离。

第二,内存保护。可以给内存页设置权限,比如只读、可写、可执行,还可以设置用户态/内核态权限。用户态进程不能访问内核页,访问违反权限的内存会触发异常。

第三,支持更大的地址空间。32位系统可以有4GB的虚拟地址空间,即使物理内存只有1GB。通过页面交换,可以把不常用的页面换出到磁盘。

第四,内存共享。多个进程可以把虚拟地址映射到同一块物理内存,实现共享内存通信。


5.2 虚拟地址如何转换成物理地址?

回答思路:

虚拟地址到物理地址的转换是通过页表完成的,由MMU硬件自动完成。

地址结构

虚拟地址分为两部分:页号和页内偏移。

比如32位系统,4KB页面,虚拟地址的高20位是页号,低12位是页内偏移。

转换过程

第一步,CPU发出虚拟地址。

第二步,MMU提取虚拟地址的页号。

第三步,MMU查页表,根据页号找到对应的物理页框号。

第四步,MMU把物理页框号和页内偏移组合,得到物理地址。

第五步,MMU访问物理地址,读取或写入数据。


5.3 MMU的作用是什么?

回答思路:

MMU(Memory Management Unit)是CPU里的一个硬件单元,负责虚拟地址到物理地址的转换和内存保护。

主要功能

第一,地址转换。把虚拟地址转换成物理地址,通过查页表完成。这是MMU最核心的功能。

第二,内存保护。检查内存访问权限,比如只读页不能写,用户态不能访问内核空间。如果违反权限,MMU产生异常,CPU跳转到异常处理函数。

工作过程

CPU发出虚拟地址,MMU自动查TLB,如果命中,直接得到物理地址。如果TLB miss,MMU查页表,找到物理地址,同时更新TLB。

如果页表项无效,MMU产生缺页异常,操作系统处理。


5.4 什么是内存分页?页表是什么?

回答思路:

内存分页就是把内存分成一块一块固定大小的页面,一般是4KB。

为什么要分页

虚拟内存需要建立虚拟地址到物理地址的映射。如果按字节映射,映射表会非常大。分页后,只需要记录每个页的映射关系,大大减少了映射表的大小。

页和页框

虚拟地址空间分成的块叫"页"(Page),物理内存分成的块叫"页框"(Page Frame)。

比如虚拟地址的第0页可以映射到物理内存的第5页框,第1页映射到第3页框。虚拟地址连续,但物理地址可以不连续。

页表

页表就是记录"虚拟页号→物理页框号"映射关系的表。

每个进程有自己的页表。MMU根据虚拟地址的页号查页表,找到对应的物理页框号,再加上页内偏移,就得到了物理地址。

页表项除了物理页框号,还有权限标志位,比如可读、可写、可执行、用户态/内核态等。


5.5 Cache是什么?为什么需要?工作原理是什么?

回答思路:

Cache是CPU和内存之间的高速缓存,用来加速内存访问。

为什么需要Cache

CPU速度很快,内存速度慢,差距很大。CPU直接访问内存太慢,会浪费很多时间等待。

Cache速度接近CPU,虽然容量小,但因为程序有局部性(访问的数据集中在一小块区域),所以够用。

Cache怎么工作

CPU要读取数据时,先看Cache里有没有。

如果有(Cache命中),直接从Cache读,很快。

如果没有(Cache miss)或Cache被标记为无效(比如被手动invalidate,或者被硬件标记),从内存读取,同时把数据放到Cache里,下次访问就快了。

CPU要写入数据时,也是先写到Cache。

至于什么时候写回内存,取决于Cache的写策略(Write-through立即写回,Write-back延迟写回),也可以手动flush立即写回。

Cache Line

Cache不是按字节管理的,而是按Cache Line管理,一般是64字节。

读一个字节时,会把整个64字节的Cache Line都加载进来。这样相邻的数据下次访问就不用再去内存读了。

Cache属性

内存区域可以设置不同的Cache属性:

Cacheable(可缓存):正常内存区域,可以使用Cache加速访问。

Non-cacheable(不可缓存):直接访问内存,不经过Cache。一般用于硬件寄存器、DMA缓冲区。

Write-through(写穿透):写操作同时更新Cache和内存。

Write-back(写回):写操作只更新Cache,稍后再写回内存。

刷Cache和无效Cache

Flush(刷Cache):把Cache里修改过的数据写回内存。DMA发送数据前要flush,保证内存里是最新数据。

Invalidate(无效Cache):把Cache里的数据标记为无效,下次访问会重新从内存读。DMA接收数据后要invalidate,保证读到的是DMA写入的新数据,而不是Cache里的旧数据。


6 调度算法

6.1 什么是进程调度?调度的时机有哪些?

回答思路:

进程调度就是操作系统决定让哪个进程在CPU上运行。多个进程轮流在CPU上执行,每个进程运行一段时间,然后切换到下一个进程。

为什么需要调度

CPU数量有限,但进程很多。通过调度,让多个进程轮流使用CPU,实现"并发"执行。

调度的时机

第一,进程主动阻塞。进程等待I/O、等待锁、调用sleep等,主动放弃CPU,进入阻塞状态。调度器选择其他就绪进程运行。

第二,时间片用完。分时系统中,每个进程分配一个时间片。时间片用完后,即使进程还想继续运行,也要切换到其他进程,保证公平。

第三,进程被唤醒。阻塞的进程等待的事件发生了(I/O完成、锁被释放),进程被唤醒进入就绪状态。如果它的优先级更高,可能立即抢占当前进程。

第四,进程创建或终止。新进程创建后进入就绪状态,可能触发调度。进程执行完毕或被杀死,调度器选择下一个进程运行。

第五,中断返回。中断处理完毕返回时,调度器检查是否需要切换进程。比如定时器中断可能导致时间片用完。


6.2 常见的调度算法有哪些?各有什么特点?

回答思路:

常见的调度算法有这几种:

先来先服务(FCFS)

按照进程到达的顺序调度,先到先执行。

优点是简单公平,实现容易。

缺点是平均等待时间长,如果前面有个长进程,后面的短进程要等很久。不适合交互式系统。

最短作业优先(SJF)

选择执行时间最短的进程先执行。

优点是平均等待时间最短,理论上最优。

缺点是要预知进程执行时间,实际很难做到。而且长进程可能饿死。

时间片轮转(RR)

每个进程分配一个时间片,时间片用完就切换到下一个进程,循环执行。

优点是公平,响应时间好,适合分时系统。

缺点是上下文切换开销,时间片太小开销大,时间片太大响应慢。

优先级调度

每个进程有优先级,选择优先级最高的进程执行。

优点是灵活,可以根据重要性分配CPU。

缺点是低优先级进程可能饿死。可以用优先级老化解决,等待时间长了就提高优先级。

多级反馈队列

多个就绪队列,不同队列不同优先级和时间片。新进程进入高优先级队列,如果时间片用完还没执行完,降到下一级队列。

优点是综合了多种算法的优点,既照顾短进程,又不会饿死长进程。

缺点是实现复杂。

Linux用的是CFS(完全公平调度器),基于虚拟运行时间,保证每个进程获得公平的CPU时间。


6.3 什么是实时调度?与普通调度有什么区别?

回答思路:

实时调度是为实时系统设计的调度算法,保证任务在规定时间内完成。

实时系统的特点

实时系统要求任务在截止时间(Deadline)前完成,否则就是失败。

比如汽车的刹车系统,必须在几毫秒内响应,晚了就出事故。

硬实时和软实时

硬实时:任务必须在截止时间前完成,超时就是系统失败。比如飞机控制系统。

软实时:任务最好在截止时间前完成,偶尔超时可以接受。比如视频播放,偶尔丢帧可以接受。

实时调度算法

最早截止时间优先(EDF):选择截止时间最早的任务执行。

速率单调调度(RMS):周期任务,周期越短优先级越高。

与普通调度的区别

普通调度追求公平、吞吐量、响应时间,但不保证截止时间。

实时调度追求可预测性,保证任务在截止时间前完成。

实时调度要做可调度性分析,确保所有任务都能按时完成。如果不能,就拒绝接受新任务。


6.4 什么是抢占式调度和非抢占式调度?

回答思路:

抢占式和非抢占式是调度的两种方式。

非抢占式调度

进程一旦获得CPU,就一直执行,直到主动放弃CPU(阻塞、终止)或时间片用完。

其他进程不能强制抢占CPU。

优点是实现简单,上下文切换少。

缺点是响应时间差,如果一个进程一直占用CPU,其他进程要等很久。

抢占式调度

高优先级进程可以抢占低优先级进程的CPU。或者时间片用完,强制切换。

优点是响应时间好,高优先级任务能及时执行。

缺点是上下文切换多,实现复杂,要处理临界区保护。

现代操作系统的选择

Linux、Windows都是抢占式调度,保证响应时间和公平性。

RTOS也是抢占式调度,保证高优先级任务及时执行。

但内核态代码有些地方不能抢占,比如持有自旋锁时,要禁止抢占。


6.5 优先级调度可能出现什么问题?如何解决?

回答思路:

优先级调度最大的问题是优先级反转和饥饿。

优先级反转

高优先级任务等待低优先级任务持有的资源,而低优先级任务被中优先级任务抢占,导致高优先级任务长时间等待。

比如任务H(高优先级)等待锁,锁被任务L(低优先级)持有。任务M(中优先级)抢占了任务L,任务L无法释放锁,任务H就一直等待。

这就是优先级反转,高优先级任务反而比中优先级任务慢。

解决方法

优先级继承:任务L持有锁时,如果任务H等待这个锁,就临时提高任务L的优先级到任务H的优先级。任务L释放锁后,恢复原来的优先级。

优先级天花板:给每个锁设置一个优先级天花板,等于所有可能持有这个锁的任务的最高优先级。任务持有锁时,优先级提升到天花板。

饥饿问题

低优先级任务可能一直得不到执行,因为总有高优先级任务。

解决方法:

优先级老化:任务等待时间长了,就逐渐提高优先级,最终能得到执行。

公平调度:保证每个任务都能获得一定的CPU时间,不管优先级。


相关推荐
csbysj202010 小时前
Python Math: 深入探索Python中的数学模块
开发语言
是一个Bug10 小时前
Java后端开发面试题清单(50道)
java·开发语言·jvm
GIS 数据栈10 小时前
【Seggis遥感系统升级】用C++高性能服务Drogon重构软件服务架构|QPS提升300%,性能再升级!
java·开发语言·c++·重构·架构
moxiaoran575310 小时前
Go语言的接口
开发语言·后端·golang
浮尘笔记10 小时前
Go语言中的同步等待组和单例模式:sync.WaitGroup和sync.Once
开发语言·后端·单例模式·golang
oMcLin10 小时前
如何在 CentOS Stream 9 上配置并优化 PostgreSQL 15,支持高并发的数据插入与快速查询?
linux·postgresql·centos
柏木乃一10 小时前
进程(11)进程替换函数详解
linux·服务器·c++·操作系统·exec
lsx20240610 小时前
C++ 变量作用域
开发语言
ben9518chen10 小时前
Linux文件系统基础
linux·服务器·php