操作系统知识点
操作系统面试
进程和线程有什么区别?
- 进程(Process)是系统进行资源分配和调度的基本单位,线程(Thread)是CPU调度的最小单位;
- 线程依赖于进程而存在,一个进程至少有一个线程;
- 进程有自己的==独立地址空间==,线程==共享所属进程的地址空间==;
- 进程是拥有系统资源的一个独立单位,而线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器s,一组寄存器和栈),和其他线程共享本进程的相关资源如内存、I/O、cpu等;
- 在进程切换时,涉及到整个当前进程CPU环境的保存环境的设置以及新被调度运行的CPU环境的设置,而线程切换只需保存和设置少量的寄存器的内容,并不涉及存储器管理方面的操作,可见,进程切换的开销远大于线程切换的开销;
- 线程之间的通信更方便,同一进程下的线程共享全局变量等数据,而进程之间的通信需要以进程间通信(IPC)的方式进行;
- 多线程程序只要有一个线程崩溃,整个程序就崩溃了,但多进程程序中一个进程崩溃并不会对其它进程造成影响,因为进程有自己的独立地址空间,因此多进程更加健壮。
同一进程中的线程可以共享哪些数据?
- 进程代码段
- 进程的公有数据(全局变量、静态变量…)
- 进程打开的文件描述符
- 进程的当前目录
- 信号处理器/信号处理函数:对收到的信号的处理方式
- 进程ID与进程组ID
线程独占哪些资源?
- 线程ID
- 一组寄存器的值
- 线程自身的栈(堆是共享的)
- 错误返回码:线程可能会产生不同的错误返回码,一个线程的错误返回码不应该被其它线程修改;
- 信号掩码/信号屏蔽字(Signal mask):表示是否屏蔽/阻塞相应的信号(SIGKILL,SIGSTOP除外)
进程间的通信方式有哪些?
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信
第一类:传统的Unix通信机制
1. 管道/匿名管道(pipe)
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的实质:
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。
该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。
当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。
无名管道的优点:
- 简单易用
无名管道的缺点:
- 只支持单向数据流;
- 只能用于具有亲缘关系的进程之间;
- 管道的缓冲区是有限的。
2.有名管道(FIFO)
有名管道的优点:
允许无亲缘关系进程间的通信
有名管道的缺点:
- 由于有名管道的名字存在于文件系统中,内容存放在内存中,所以它长期存于系统中,使用不当容易出错。
- 管道的缓冲区是有限的。
3.信号(Signal)
- 信号可以在任何时候发给某一进程,而无需知道该进程的状态。
- 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程回复执行并传递给它为止。
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
- 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。
Linux系统中常用信号:
- SIGINT:程序终止信号。程序运行过程中,按
Ctrl+C
键将产生该信号。(强制终止不能被无视) - SIGTERM:结束进程信号。shell下执行
kill 进程pid
发送该信号。
4.消息(Message)队列
消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列是随内核持续的,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。
优点:可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便
缺点:信息的复制需要额外消耗 CPU 的时间,不适宜于信息量大或操作频繁的场合
5. 共享内存(share memory)
- 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
- 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
- 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
- 优点:无须复制,快捷,信息量大
- 缺点:
- 通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,因此要考虑进程间的读写操作的同步问题
- 问题利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信
6.信号量(semaphore)
- PV操作
7. 套接字(socket)
套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
套接字特性
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。
线程之间的通信方式
- 锁机制:包括互斥锁/量(mutex)、读写锁(reader-writer lock)、自旋锁(spin lock)、条件变量(condition)
- 互斥锁/量(mutex):提供了以排他方式防止数据结构被并发修改的方法。
- 读写锁(reader-writer lock):允许多个线程同时读共享数据,而对写操作是互斥的。
- 自旋锁(spin lock)与互斥锁类似,都是为了保护共享资源。互斥锁是当资源被占用,申请者进入睡眠状态;而自旋锁则循环检测保持者是否已经释放锁。
- 条件变量(condition):可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
- 信号量机制(Semaphore)
- 无名线程信号量
- 命名线程信号量
- 信号机制(Signal):类似进程间的信号处理
- 屏障(barrier):屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。
锁机制
读写锁
也称之为 共享-独占锁,一般用在读和写的次数有很大不同的场合。即对某些资源的访问会出现两种情况,一种是访问的排他性,需要独占,称之为写操作;还有就是访问可以共享,称之为读操作。
互斥锁
是最常见的线程同步方式,它是一种特殊的变量,它有 lock 和 unlock 两种状态,一旦获取,就会上锁,且只能由该线程解锁,期间,其他线程无法获取
什么叫优先级反转?如何解决?
高优先级的进程等待被一个低优先级进程占用的资源时,就会出现优先级反转,即优先级较低的进程比优先级较高的进程先执行。此处详细解释优先级反转带来的问题:如果有一个中等优先级的进程将低优先级的进程抢占,那么此时低优先级的进程无法正常进行并在后续释放被占用的资源,导致高优先级的任务一直被挂起,直到中等优先级的进程完成后,低优先级的进程才可以继续并在后续释放占用的资源,最后高优先级的进程才可以执行。导致的问题就是高优先级的进程在中等优先级的进程调度之后。
解决方法:
- 优先级天花板(priority ceiling):当任务申请某资源时,把该任务的优先级提升到可访问这个资源的所有任务中的最高优先级,这个优先级称为该资源的优先级天花板。简单易行。
- 优先级继承(priority inheritance):当任务A申请共享资源S时,如果S正在被任务C使用,通过比较任务C与自身的优先级,如发现任务C的优先级小于自身的优先级,则将任务C的优先级提升到自身的优先级,任务C释放资源S后,再恢复任务C的原优先级。(让低优先级暂时变高,赶紧释放占着的资源)
什么是僵尸进程?
一个子进程结束后,它的父进程并没有等待它(调用wait或者waitpid),那么这个子进程将成为一个僵尸进程。僵尸进程是一个已经死亡的进程,但是并没有真正被销毁。它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程表中保留一个位置,记载该进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)供父进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程可能会一直留在系统中直到系统重启。
危害:占用进程号,而系统所能使用的进程号是有限的;占用内存。
以下情况不会产生僵尸进程:
- 该进程的父进程先结束了。每个进程结束的时候,系统都会扫描是否存在子进程,如果有则用Init进程接管,成为该进程的父进程,并且会调用wait等待其结束。
- 父进程调用wait或者waitpid等待子进程结束(需要每隔一段时间查询子进程是否结束)。wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。waitpid则可以加入
WNOHANG
(wait-no-hang)选项,如果没有发现结束的子进程,就会立即返回,不会将调用waitpid的进程阻塞。同时,waitpid还可以选择是等待任一子进程(同wait),还是等待指定pid的子进程,还是等待同一进程组下的任一子进程,还是等待组ID等于pid的任一子进程; - 子进程结束时,系统会产生
SIGCHLD
(signal-child)信号,可以注册一个信号处理函数,在该函数中调用waitpid,等待所有结束的子进程(注意:一般都需要循环调用waitpid,因为在信号处理函数开始执行之前,可能已经有多个子进程结束了,而信号处理函数只执行一次,所以要循环调用将所有结束的子进程回收); - 也可以用
signal(SIGCLD, SIG_IGN)
(signal-ignore)通知内核,表示忽略SIGCHLD
信号,那么子进程结束后,内核会进行回收。
什么是孤儿进程?
一个父进程已经结束了,但是它的子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程会被Init(进程ID为1)接管,当这些孤儿进程结束时由Init完成状态收集工作。
线程同步有哪些方式?
为什么需要线程同步:线程有时候会和其他线程共享一些资源,比如内存、数据库等。当多个线程同时读写同一份共享资源的时候,可能会发生冲突。因此需要线程的同步,多个线程按顺序访问资源。
- 互斥量 Mutex:互斥量是内核对象,只有拥有互斥对象的线程才有访问互斥资源的权限。因为互斥对象只有一个,所以可以保证互斥资源不会被多个线程同时访问;当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源;
- 信号量 Semaphore:信号量是内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。信号量对象保存了最大资源计数和当前可用资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数大于0,就可以发出信号量信号,如果为0,则将线程放入一个队列中等待。线程处理完共享资源后,应在离开的同时通过
ReleaseSemaphore
函数将当前可用资源数加1。如果信号量的取值只能为0或1,那么信号量就成为了互斥量; - 事件 Event:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。事件分为手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。
- 临界区 Critical Section:任意时刻只允许一个线程对临界资源进行访问。拥有临界区对象的线程可以访问该临界资源,其它试图访问该资源的线程将被挂起,直到临界区对象被释放。
互斥量和临界区有什么区别?
互斥量是可以命名的,可以用于不同进程之间的同步;而临界区只能用于同一进程中线程的同步。创建互斥量需要的资源更多,因此临界区的优势是速度快,节省资源。
什么是协程?
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁 的访问全局变量,所以上下文的切换非常快。
协程多与线程进行比较?
- 一个线程可以拥有多个协程,一个进程也可以单独拥有多个协程。
- 线程进程都是同步机制,而协程则是异步
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
产生死锁的四个必要条件(破坏死锁的四个条件)
互斥条件
一个资源只能由进程独占使用,若别的进程也要求该资源,则需等待直至其占用者释放。
请求和保持条件
进程已经保持了至少一个资源,但又提出新的资源请求,而该资源又被其他进程占有。此时请求进程阻塞,但又不释放自己获得的资源。
非剥夺条件
进程所获得的资源在未使用完之前,不能被其他进程夺走,而只能由自身释放。
循环等待条件
存在一个等待进程集合${p_0,p_1,…,p_n}$,$p_0$在等待$p_1$占用的资源,$p_1$在等待$p_2$占用的资源,$p_n$在等待$p_0$占用的资源。
分段和分页
- 页式存储:用户空间划分为大小相等的部分称为页,内存空间划分为同样大小的区域称为页框,分配时以页为单位按进程需要的页数分配,逻辑上相邻的页物理上不一定相邻
- 段式存储:用户进程地址空间按照自身逻辑关系划分为若干个段(如代码段,数据段,堆栈段),内存空间被动态划分为长度不同的区域,分配时以段为单位,逻辑上相邻的段物理上也相邻占据连续空间,各段可以不相邻。
- 段页式存储:用户进程先按段划分,段内再按页划分,内存划分和分配按页。
外碎片:外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。 内碎片:内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间; 频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。
缓冲区溢出问题
C 语言使用运行时栈来存储过程信息。每个函数的信息存储在一个栈帧中,包括寄存器、局部变量、参数、返回地址等。C 对于数组引用不进行任何边界检查,因此对越界的数组元素的写操作会破坏存储在栈中的状态信息,这种现象称为缓冲区溢出。缓冲区溢出会破坏程序运行,也可以被用来进行攻击计算机,如使用一个指向攻击代码的指针覆盖返回地址。
防范方式
防范缓冲区溢出攻击的机制有三种:随机化、栈保护和限制可执行代码区域。
- 随机化:包括栈随机化(程序开始时在栈上分配一段随机大小的空间)和地址空间布局随机化(Address-Space Layout Randomization,ASLR,即每次运行时程序的不同部分,包括代码段、数据段、栈、堆等都会被加载到内存空间的不同区域),但只能增加攻击一个系统的难度,不能完全保证安全。
- 栈保护:在每个函数的栈帧的局部变量和栈状态之间存储一个随机产生的特殊的值,称为金丝雀值(canary)。在恢复寄存器状态和函数返回之前,程序检测这个金丝雀值是否被改变了,如果是,那么程序异常终止。
- 限制可执行代码区域:内存页的访问形式有三种:可读、可写、可执行,只有编译器产生的那部分代码所处的内存才是可执行的,其他页限制为只允许读和写。
磁盘调度
过程:磁头(找到对应的盘面);磁道(一个盘面上的同心圆环,寻道时间);扇区(旋转时间)。为减小寻道时间的调度算法:
- 先来先服务
- 最短寻道时间优先
- 电梯算法:电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
异常和中断
中断是指 CPU 对系统发生某事件时的这样一种响应: CPU 暂停正在执行的程序,在保留现场后自动地转去执行该事件的中断处理程序;执行完后,再返回到原程序的断点处继续执行。
- 中断——外部事件引起,正在运行的程序所不期望的
- 异常——内部执行指令引起