操作系统面试题
进程管理
-
说说动态库和静态库的区别?
- 编译和链接阶段
• 静态库:在编译时被链接到程序中,库中的代码会被复制到生成的可执行文件中。编译器在编译程序时,将需要的静态库函数代码直接拷贝到可执行文件中,因此生成的程序文件较大。
• 动态库:在编译时不会被直接包含到程序中,程序只会包含对动态库的引用。运行时,系统会加载动态库,并在运行时将程序对动态库的调用重定向到库的实现上。这使得编译生成的可执行文件较小。 - 文件大小
• 静态库:由于将库的内容嵌入到可执行文件中,生成的可执行文件较大。
• 动态库:生成的可执行文件较小,因为它仅包含对动态库的引用。真正的库代码存储在外部文件中。 - 内存占用
• 静态库:每个进程会各自加载并包含一份静态库代码,多个进程共享静态库代码的可能性较低,因此内存占用相对较高。
• 动态库:多个进程可以共享同一份动态库代码,只有在第一次加载时才会被加载到内存中,后续使用时可直接共享,节省了内存空间。
- 编译和链接阶段
-
讲讲上下文(进程、线程)切换的过程?
- 进程切换
进程切换需要在两个独立的进程之间切换执行,涉及到相对较重的上下文保存和恢复。进程切换时,操作系统会: - 保存当前进程的上下文:
o 将当前进程的寄存器(包括程序计数器PC、栈指针SP等)值保存在进程控制块(PCB)中。
o 保存当前进程的内存映射信息,如页表、虚拟地址空间等。 - 更新进程调度信息:
o 根据调度算法(如时间片轮转、优先级调度等),选择下一个要运行的进程。
o 更新调度信息,如上一个进程的状态设置为“阻塞”或“就绪”,新的进程设置为“运行”。 - 恢复新进程的上下文:
o 恢复新的进程的寄存器内容、程序计数器等,重新加载页表信息,使CPU和内存指向新的地址空间。
o 在多核系统中,可能需要刷新TLB(Translation Lookaside Buffer)缓存,以确保新的虚拟地址空间正常访问。 - 重新开始执行:
o CPU执行恢复的程序计数器所指向的指令,继续运行新进程。
由于进程切换需要重新加载内存地址空间,涉及内存管理信息的切换,开销较大。 - 线程切换
线程切换发生在同一进程内的不同线程之间,相比进程切换,线程切换不需要重新加载进程的地址空间,因此开销更小。线程切换过程为: - 保存当前线程的上下文:
o 将当前线程的寄存器(包括程序计数器PC、栈指针SP等)值保存在线程控制块(TCB)中。
o 因为同一进程中的线程共享相同的地址空间,不涉及页表切换,因此不需保存内存映射信息。 - 更新线程调度信息:
o 使用线程调度算法选择要运行的下一个线程,将当前线程的状态更新为“阻塞”或“就绪”,新线程状态更新为“运行”。 - 恢复新线程的上下文:
o 恢复新的线程的寄存器内容和程序计数器,使其指向新线程的栈和代码位置。 - 重新开始执行:
o CPU执行恢复的程序计数器所指向的指令,继续运行新线程。
线程切换的开销更低,因为不涉及内存空间的切换和内存映射的重载,仅需要保存和恢复少量的寄存器。
- 进程切换
-
进程和线程的区别?
进程和线程在内核都用一个task_struct结构体表示,两者区别在于资源的共享程度,由task_struct中各字段是否共享决定,而字段是否共享则由clone()创建新task时的参数flag决定的。
进程:拥有独立虚拟地址空间,包括代码段、数据段和堆栈段
线程:同一线程组的线程共享父进程的虚拟地址空间(线程组id=父进程id)
线程拥有独立的栈空间和寄存器,共享堆空间
最大的区别在于上下文切换:
进程
虚拟内存与物理内存的映射需要查页表,TLB缓存记录映射关系,可以加快查询速度,而上下文切换时会切换页表(更新cr3寄存器指向新的页表),TLB失效,切换到新进程后查页表速度很慢,因为缓存命中率低。切换页表是进程切换的主要开销
线程
由于同一进程内的线程共享虚拟内存地址空间,没有切换页表(即更新cr3寄存器),也不会刷新TLB,内核只需保存当前线程的寄存器状态、程序计数器、堆栈指针等,开销相对较小
(CR3寄存器:CR3寄存器保存的是当前进程的页目录的物理地址) -
描述系统调用过程?
用户态调用标准库函数发起系统调用,会触发0x80中断或syscall指令切换到内核态。进入内核态根据eax寄存器保存的系统调用号,查系统调用表找到对应处理函数并执行,执行结果保存在eax寄存器,再使用 iret 指令将 CPU 恢复到用户态 -
讲讲用户态到内核态的切换过程?
用户态通过系统调用、异常、中断三种方式可以陷入内核态
进入内核态前首先保存用户态上下文,包括程序计数器(PC)、栈指针(SP)、通用寄存器和状态寄存器,每个进程的上下文保存到独立的内核栈中
接着修改CS寄存器(代码段寄存器)的特权级别位CS.RPL(00内核态,11用户态)来实现cpu权限的转换
使用iret指令将CS.RPL = 11恢复到用户态 -
进程间通信方式了解哪些?
共享内存mmap,socket通信、管道、消息队列
其中共享内存需要考虑多进程同时修改的竞态问题,即进程间同步的问题,使用信号量解决:初始化一个信号量=1,使用PV操作:第一个进程执行P操作将信号量-1,第二个进程判断信号量=0会阻塞等待,第一个进程操作完成后执行V操作将信号量+1,这时信号量>0,其他进程可以访问共享资源 -
线程同步方式?
原子操作、锁和条件变量:
1.原子操作:c++11提供原子变量,对原子变量的修改保证原子性,底层用cpu硬件提供的CAS原子指令
2.互斥锁、自旋锁。
-互斥锁:底层依赖CAS,当一个线程持有锁,会临时屏蔽中断保证操作的连续,没有获取到锁的线程被记录到一个阻塞队列中阻塞等待,当锁再次可用时唤醒一个线程并恢复其上下文开始调度运行。
-自旋锁:同样依赖CAS,当一个线程持有锁,会临时屏蔽中断保证操作的连续,没有获取到锁的线程会循环检查锁状态,避免了线程从阻塞到恢复的上下文切换的开销。为了避免自旋等待时间过长导致性能下降,通常会对自旋次数进行限制,达到自选次数上限会被阻塞。
两者都会维护一个表示锁状态的标志位,0表示锁空闲,1表示锁被占用。当一个线程尝试获取锁时,它会尝试使用原子操作(CAS)将锁状态从0修改为1
-条件变量:
条件变量必须配合互斥锁实现,即调用pthread_cond_wait前后必须上锁、释放锁
因为pthread_cond_wait内多个子行为并不是原子性的,所以需要使用锁。
条件变量的原理:1.先释放锁,然后使线程阻塞,加入等待队列。2.被条件变量唤醒后,先获取锁,函数返回,返回后线程的状态与调用pthread_cond_wait前一致,都为获取锁的状态。 -
讲讲CAS与原子操作?
CAS是一种用于实现原子性操作的机制Compare-And-Swap),一般由硬件提供支持,软件层面也可以实现。它的基本操作包括三个参数:- 内存地址:需要进行比较和交换的内存位置。
- 预期值(expected):希望内存位置当前的值。
- 新值(new):如果内存位置的值等于预期值,则将其更新为这个新值。
工作原理
CAS的工作原理如下:
• CAS操作会先读取内存地址的当前值。
• 然后将这个值与预期值进行比较:
o 如果它们相等,则将内存地址的值替换为新值,并返回成功的结果(通常为真)。
o 如果它们不相等,则不进行任何操作,并返回失败的结果(通常为假)。
原子操作:一个操作要么完全执行成功,要么完全不执行,无法被其他操作打断。称为原子操作。
-
讲讲fork()?
1.调用fork() 首先创建一个task_struct结构体
2.底层调用copy_process()复制父进程的虚拟地址空间、文件描述符等信息(除了pid其他全部复制)
3.复制虚拟地址空间时,使用写时复制:
只复制页表(记录虚拟地址-物理地址映射关系),此时父子进程的页表映射在同一块物理地址。并将父进程和子进程的相同页表标记为只读,当父进程或子进程试图修改标记为只读的共享内存区域,为该进程创建一个新的物理内存页并映射到虚拟地址,并将此页面(父和子)设置为可读写并进行修改。
如果只读虚拟内存,父子进程会访问到同一块物理内存,因为页表是相同的
4.内核会将新创建的子进程的状态设置为就绪态,等待被调度。Fork将子进程的 PID 返回给父进程,并将 0 返回给子进程 -
进程调度策略了解哪些?
CFS公平调度策略(linux)
用红黑树记录每一个task的运行时间,每次选择运行时间最短的task调度运行,而这个运行时间是虚拟运行时间,会根据其nice值*实际运行时间,也就是加权的,故nice值小的task实际会被分配到更多cpu时间。选择最小ask运行时移除红黑树,运行一段时间后重新计算运行时间,并重新插入到红黑树,然后选择下一个最小节点。 -
后台进程有什么特点?如何实现?
后台运行:守护进程在后台运行,没有控制终端,不与用户直接交互。
生命周期长:通常随系统启动运行,直到系统关闭或服务被手动停止。
实现:
Fork创建子进程、父进程退出,该子进程会被init进程接管,成为孤儿进程 -
cpu是怎么执行指令的?
取指令、译码、执行、写回
取指令:CPU的程序计数器(PC)会存储下一条要执行的指令地址。在取指阶段,CPU从内存(RAM或高速缓存)中取出该地址处的指令,并加载到指令寄存器(IR)。同时,PC会递增,以指向下一条指令的地址。
译码:将指令解析为操作码和操作数
执行:cpu算术逻辑单元执行算术操作
写回:将计算结果保存到寄存器或内存 -
什么是死锁?如何避免?
死锁是指多个进程(或线程)相互等待对方持有的资源释放,也就是“占有并等待”的状态
无法避免,可以进行死锁检测 -
互斥锁是怎么实现的?
(同问题7.) -
条件变量是怎么实现的?
(同7.) -
中断是怎么实现的?
Cpu收到中断后先保存cpu上下文,再根据中断号查中断向量表,找到中断处理函数,处理中断后恢复上下文。 -
Linux进程和线程是怎么实现的?
(主要介绍一下task_struct中的字段,哪些实现了内存管理、进程调度、堆栈等) -
一个进程最多可以创建多少个线程?
首先,受系统资源的限制:每个线程需要内存维护栈空间和线程控制块(pcb),一个线程栈空间1-8MB,ulimit -s 命令调整每个线程的栈空间大小。减小栈空间可以允许更多线程。
其次,系统会限制单进程的线程数,由内核参数决定,参数可以修改 -
epoll最多能接受多少个连接?
首先epoll不限制能管理的连接(fd)数量,所以这个问题是在问操作系统可以创建多少tcp连接
1.系统参数限制了单进程能创建的最大文件描述符fd的数量,默认1024
2. 每个连接都会占用一定的内存,包括套接字缓冲区、TCP 控制块
3. tcp连接,它由一个四元组唯一确定(源端口,目的端口,源ip,目的ip)。对于服务端的每一个ip+端口,一个客户端最多能创建64511个连接,因为端口号是16位的,最多表示65535个端口,减去0-1023不可分配的端口只剩下64511个
获取Linux C/C++开发学习资料