操作系统的四个特性: 并发、共享、虚拟、异步 操作系统的主要功能: 进程管理、内存管理、设备管理、文件管理、用户接口
进程的三种状态: 1)运行状态:进程正在处理器上运行。 2)就绪状态:进程已处于准备运行状态。即进程获得了除处理器以外的一切所需资源,一旦得到处理器即可运行。 3)阻塞状态(等待状态):进程正在等待某一事件而暂停运行,如等待某资源(不包括处理机)为可用或等待输入/输出完成。即使处理器空闲,该进程也不能运行。
就绪状态与阻塞状态区别: 就绪状态是指进程仅缺少处理器,只要获得处理器资源就立即执行;阻塞状态是指进程需要其他资源(除了处理器)或等待某一事件。
状态转换: 就绪状态 -> 运行状态: 处于就绪状态的进程被调度之后,获得处理器资源,进程由就绪状态转换为运行状态。 运行状态 -> 就绪状态: 处于运行状态的进程在时间片用完后,不得不让出处理器;或在可抢占式操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪状态,让高优先级的进程执行。 运行状态 -> 阻塞状态: 当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生时,就从运行状态转换为阻塞状态。 阻塞状态 -> 就绪状态: 当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。
进程和线程的主要差别在于:是不同的操作系统资源管理方式。 1)进程是系统进行资源分配和调度的一个独立单位;线程是CPU调度和分派的基本单位。 2)进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响。 线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量。但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,多进程的程序比多线程的程序健壮。 3)进程切换时,耗费资源较大,效率要差一些。对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。 4)一个程序至少有一个进程,一个进程至少有一个线程。 5)线程的粒度小于进程,使得多线程程序的并发性高。 6)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高程序运行效率。 7)每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存在应用程序中,有应用程序提供多个线程执行控制。 8)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
优缺点: 线程执行开销小,但不利于资源的管理和保护;进程正好相反。
临界资源: 在同一时间只能被一个进程所占用的资源,叫做临界资源。比如:物理上的打印机、硬盘或内存中被多个进程所共享的变量和数据。 对于临界资源的访问,必须是互斥进行。当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。进程内访问临界资源的代码被称为临界区。
临界区的访问过程: 1)进入区:查看临界区是否可以访问,如果可以访问,则转到步骤二,否则进程会被阻塞; 2)临界区:在临界区进行操作; 3)退出区:清除临界区被占用的标志; 4)剩余区:进程与临界区不相关部分的代码;
同步和互斥: 同步: 进程间直接制约关系。为完成某种任务而建立的两个或多个进程。这些进程需要在某些位置上协调工作次序而等待、传递信息所产生的制约关系。 互斥: 进程间间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问临界资源。
同步机制需要遵循的原则: 1)空闲让进:当没有进程处于临界区的时候,应许可其他进程进入临界区的申请。 2)忙则等待:当前如果有进程处于临界区,若有其他进程申请进入,则必须等待,保证对临界区的互斥访问。 3)有限等待:对要求访问临界资源的进程,需要在有限时间内进入临界区,防止出现死等。 4)让权等待:当进程无法进入临界区的时候,需要释放处理器。
进程同步解决方案: 信号量、管程。
信号量方法: 信号量是一个确定的二元组(s, q),其中s是一个具有非负初值的整型变量(表示系统中某类资源的数目),q是一个初始状态为空的队列。 当s >= 0时,表示系统中当前可用资源的数目; 当s < 0时,其绝对值表示系统中因请求该类资源而被阻塞的进程数目。 除信号量的初值外,信号量的值仅能由P操作和V操作更改,操作系统利用它的状态对进程和资源进行管理。
P操作申请资源: (1)S减1; (2)若S减1后仍大于等于零,则进程继续执行; (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V操作释放资源: (1)S加1; (2)若相加结果大于零,则进程继续执行; (3)若相加结果小于等于零,则从信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。
经典同步问题: 1)生产者-消费者问题:
semaphore fullbuffers = 0; // 仓库中已填满的货架个数 semaphore emptybuffers = BUFFER_SIZE; // 仓库空闲货架个数 semaphore mutex = 1; // 生产-消费互斥信号 Producer(){ while(ture){ 生产产品item P(emptybuffers); P(mutex); item 存入仓库buffer V(mutex); V(fullbuffers); } } Consumer(){ while(ture){ P(fullbuffers); P(mutex); 从仓库buffer中取产品item V(metex); V(emptybuffers); 消费产品item } }2)读者和写者问题: 3)哲学家就餐问题: 4)理发师问题:
Linux下的进程通信手段基本上继承自Unix平台上的进程通信。其中AT&T的贝尔实验室对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了system V IPC,通信进程局限在单个计算机内;BSD形成了基于套接字(socket)的进程间通信机制。 Linux的进程间通信方式: (1) 管道 (2)消息队列 (3)信号量 (4)共享内存 (5)信号(signal) (6)套接字(socket)
死锁概念: 系统中两个或多个进程无限期地等待永远不会发生的条件,系统处于停滞状态,就是死锁。
产生死锁的原因: 系统资源不足、进程运行推进的顺序不合适、资源分配不当等。
产生死锁的四个必要条件: (1)互斥条件:一个资源每次只能被一个进程使用。 (2)请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 (3)不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。 (4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
处理死锁的四个方式: 目前处理死锁的方法可归结为以下四种: (1)预防死锁 通过设置某些限制条件,破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。 1)资源一次性分配:(破坏请求和保持条件) 2)可剥夺资源:当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件) 3)资源有序分配法:系统给每类资源赋予一个编号,每个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待条件)
(2)避免死锁 事先不采取任何限制措施去破坏产生死锁的四个必要条件。而是在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免发生死锁。 在系统分配资源前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,让进程等待。 代表算法:银行家算法 思路: 1)当进程首次申请资源时,要测试进程对资源的最大需求量。如果系统现存的资源可以满足最大需求量,则按当前的申请量分配资源;否则,推迟分配。 2)当进程在执行中,继续申请资源时,先测试该进程已占有的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过,则拒绝分配资源;若没有超过,则测试系统现存的资源能否满足该进程尚需的最大资源量:若满足,则按当前的申请量分配资源,否则推迟分配。 安全状态: 系统能按某种进程推进顺序,为每个进程分配其所需的资源,直至满足每个进程对资源的最大需求,使每个进程都可以顺序完成。此时进程p1,p2,…,pn为安全序列,若系统无法找到一个安全序列,则称系统处于不安全状态。
(3)检测死锁 无须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区。此方法允许系统在运行过程中发生死锁,但可以通过系统所设置的检测机制,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,将死锁清除掉。
(4)解除死锁 与监测死锁配套的一种措施。当监测到系统中已经发生死锁时,须将进程从死锁状态中解脱出来。 死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但实现难度较大。 1)剥夺资源:从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态; 2)撤销进程:直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可用,死锁状态消除为止; 3)进程回退:让一个或多个进程回退到足以避免死锁的状态,进程回退时释放资源而不是剥夺,要求系统保持进程的历史信息,设置还原点。
调度种类: 高级调度:又称作业调度,决定把后备作业调入内存运行; 中级调度:又称在虚拟存储器中引入,在内、外存交换区进行进程对换; 低级调度:又称进程调度,决定就绪队列的某进程获得CPU
抢占式调度:操作系统将正在运行的进程强行暂停,有调度程序将CPU分配给其他就绪进程。
调度算法: (1)先来先服务(FCFS):按作业或进程到达的先后顺序一次调度。 (2)短进程优先(SJF):从就绪队列中选择估计时间最短的进程进行处理。 (3)高优先权调度算法(抢占式、非抢占式) (4)高响应比优先(HRN):响应比=(等待时间+要求服务时间)/要求服务时间。 (5)时间片轮转(RR):按到达的先后将进程放入队列中,然后给队首进程分配CPU时间片,时间片用完之后计数器发出中断,暂停当前进程并将其放到队列尾部。 (6)多级反馈队列调度:设置多个就绪队列并为每个队列设置不同的优先级,第一个队列优先级最高,其余依次递减。优先级越高的队列分配的时间片越短,进程到达之后按FCFS放入第一个队列,如果调度执行之后没有完成,那么放到第二个队列尾部等待调度;如果第二次调度没有完成,放入第三队列尾部…. 只有当前一个队列为空的时候才会去调度下一个队列的进程。
内核态和用户态是操作系统的两种运行级别。 (1)当程序运行在ring3特权级别上,则程序运行在用户态。只能执行普通用户指令,不能执行特权指令。 当程序运行在ring0特权级别上,则程序运行在内核态。可以执行任何指令。 (2)运行在内核态的代码不受任何的限制,可以自由地访问任何有效地址,进行端口访问。 运行在用户态的代码则要受到处理器的诸多检查,只能访问映射到其地址空间的页表项中规定的在用户态下可以访问页面的虚拟地址。 (3)每个进程都有自己的内核栈和用户栈。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。当进程处于用户态时,执行的用户态代码会使用进程的用户栈。 (4)当CPU处于内核态时,可以随意进入用户态;而当CPU处于用户态时,用户从用户态切换到内核态只有在系统调用、异常和中断两种情况下发生。 (5)Linux进程的4GB地址空间,3G-4G部分是共享的,是内核态的地址空间。内核态地址空间存放整个内核的代码和所有的内核模块,以及内核所维护的数据。一般程序开始时位于用户态,当程序需要使用系统资源时,就必须通过系统调用进入内核态。处理器切换到ring0,然后进入3G-4G中的内核地址空间去执行这些代码完成操作;完成后,切换回ring3,回到用户态。
(1)进程的堆栈 内核在创建进程时,在创建task_struct的同时,会为进程创建相应的栈。每个进程会有两个栈,一个用户栈(8MB),存在于用户空间;另一个内核栈(8KB),存在于内核空间。当进程在用户空间运行时,CPU堆栈指针寄存器里面的内容是用户栈空间地址,使用用户栈。当进程在内核空间时,CPU堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
(2)进程用户栈 当进程因为中断或者系统调用而陷入内核态执行时,进程所使用的栈也要从用户栈转到内核栈,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
进程从用户态转到内核态的时候,进程的内核栈总为空。因为进程在用户态执行时,使用的是用户栈;当进程陷入到内核态时,内核栈保存进程在内核态执行的相关信息。一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复。因此,每次进程从用户态陷入内核的时候,得到的内核栈都是空的。
进程陷入内核态后,先把用户态堆栈地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成用户栈向内核栈的转换。当进程从内核态恢复到用户态执行时,在内核态执行的最后将保存在内核栈里面的用户栈地址恢复到堆栈指针寄存器即可。
(3)内核栈的实现 在Linux内核4.12 源码目录 Documentation/x86/kernel-stacks: Kernel stacks on x86-64 bit ————————— Most of the text from Keith Owens, hacked by AK x86_64 page size (PAGE_SIZE) is 4K. Like all other architectures, x86_64 has a kernel stack for every active thread. These thread stacks are THREAD_SIZE (2*PAGE_SIZE) big. These stacks contain useful data as long as a thread is alive or a zombie. While the thread is in user space the kernel stack is empty except for the thread_info structure at the bottom.
1. 操作系统面试重难点总结