linux同步机制-进程同步和调度
一、Linux进程内存空间
1.1 内核空间和用户空间
Linux采用两级保护机制:0级供内核使用、3级供用户程序使用。在32位Linux操作系统中,每个进程都有各自的私有用户空间(0~3GB),这个空间对系统中的其它进程是不可见的,最高的1GB虚拟内核空间为所有进程以及内核所共享。
针对linux操作系统而言:
- 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间;
- 而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间;
每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
需要注意的是:内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。
1.2 进程内存布局
Linux进程标准的内存段布局,如下图所示,地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。
1.3 内核态和用户态
- 当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈;
- 当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈;
程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:
- 内核态,运行于进程上下文,内核代表进程运行于内核空间;
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
- 用户态,运行于用户空间;
1.4 中断上下文
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
中断时,内核不代表任何进程运行,它一般只访问内核空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。
简单来说,中断发生以后,CPU跳到内核设置好的中断处理代码中去,由这部分内核代码来处理中断。这个处理过程中的上下文就是中断上下文。
注意:中断上下文代码不允许睡眠,也不允许调用那些可能会引起睡眠的函数。
1.5 SMP和UP
UP(Uni-Processor):系统只有一个处理器单元,即单核CPU系统;
SMP(Systemtric Multi-Processors):系统有多个处理器单元。各个处理器之间共享总线、内存等等。在操作系统看来,各个处理器之间内有区别。
二、Linux进程中同步
用户进程通常不需要加锁,因为访问的是自己的局部空间,除非有内存共享或者多线程。内核数据通常被所有进程,中断,软中断共享,所以通常都需要加锁。
2.1 进程同步问题
我们的Linux操作系统是一个多任务操作系统,运行在操作系统上的进程宏观上并行运行的,这样系统就会产生一些问题,比如有的资源、比如显示器,CPU同一时间肯定只能有一个程序在使用,多个程序肯定不能同时使用显示器,这就是互斥关系。为了解决这种问题,当一个进程获取到资源后,另一个进程必须等待,等到进程释放资源后,另一个进程才可使用,这就是同步关系。
2.2 临界资源
像上面我们所说的一次只能被一个进程所占用的资源就是临界资源,典型的临界资源比如物理上的CPU、显示器、打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
2.3 临界区
对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。
对临界资源的互斥访问了逻辑上可以分为四个部分:
- 进入区:负责检查是否可进入临界区,如果可进入,则应该设置正在访问临界资源的标志(“上锁”),以阻止其他进程同时进入临界区;
- 临界区:访问临界资源的代码;
- 退出区:负责解除正在访问临界资源的标志(“解锁”);
- 剩余区:做其他处理;
注意:进入区和退出区是负责实现互斥的代码段。
这样,可把一个访问临界资源的循环进程描述如下:
repeat entry section critical section; exit section remainder section; until false;
2.4 进程同步解决方案
为实现进程互斥的进入自己的临界区,可用软件方法,更多的是在系统中设置专门的同步机构来协调各进程间的运行。所有同步机制都应该遵循下述准则:
- 空闲让进:当无进程在互斥区时,任何有权使用互斥区的进程可进入;
- 忙则等待:当已有进程出于临界区时,表明临界资源正则被访问,因而其它师徒进入临界区的进程必须等待,以保证对临界资源的互斥访问;
- 多中择一:当没有进程在临界区,而同时有多个进程要求进入临界区,只能让其中之一进入临界区,其他进程必须等待;
- 有限等待:对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入死等状态;
- 让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入忙等状态;;
- 平等竞争:任何进程无权停止其它进程的运行,进程之间相对运行速度无硬性规定;
常用的进程同步方式有:
- 互斥锁;等价于count=1情况下的信号量。;
- 原子操作;原子操作不可能被其他的任务给调开,一切(包括中断),针对单个变量;
- 自旋锁:使用忙等待锁来确保互斥锁的一种特别方法,针对是临界区;
- 信号量;包括一个变量及对它进行的两个原语操作,此变量就称之为信号量,针对是临界区;
最后我们大概总结以下上面几种同步机制的区别,后面会分别介绍:
进程同步机制 | 原子操作 | 互斥锁 | 信号量 | 自旋锁 |
实现方式 | - | 先自旋等待、后睡眠 | 睡眠等待 | 自旋等待 |
中断内使用 | 可以 | 不可以(中断里不可以睡眠) | 不可以(中断里不可以睡眠) | 可以 |
临界区可以调用睡眠相关函数 | - | 可以 | 可以 | 不可以(单核下,禁止内核抢占,系统将不响应任何操作) |
禁止内核抢占 | 否 | 否 | 否 | 是(在单核非抢占的内核下,自旋锁是没用的,是空操作) |
适用于长时间持有锁场景 | - | 可以 | 可以 | 不可以 |
执行效率 | - | 比信号量高 | 低(需要进程切换) | 高(没有进程切换) |
实现原理 | ldrex 、strex独占访问指令 | MCS锁机制 |
raw_spin_lock_irqsave、raw_spin_unlock_irqrestore |
ldrex 、strex独占访问指令 |
三、进程调度
3.1 进程调度概念
linux内核管理了系统的有限资源,当有多个进程(或多个进程发出的请求)要使用这些资源时,因为资源的有限性,必须按照一定的原则来选择进程(请求)来占用资源,这就是调度。目的是控制资源使用者的数量,选取资源使用者许可来占用资源。
进程调度主要包含以下重要功能:
- 就绪队列:为了提供进程调度的效率,应事先将系统中所有就绪的进程按照一定的方式排成一个或者多个队列,以便调度程序能最快的找到它;
- 基本调度算法:调度程序按照某种算法如优先数算法、轮转法等,从就绪队列中选取一个进程;
- 进程上下文切换:保存当前进程的上下文,装入选中进程的上下文;装载进程需要将选中进程的状态改为运行状态,由分派程序把CPU分配给该进程,并为进程恢复进程上下文现场,然后再把CPU的控制权交给该进程,让它从上一次的断点处开始运行;
3.2 进程上下文
为了控制进程执行,内核必须有能力挂起正在CPU中运行的进程,并恢复挂起的某个进程。这被称为进程切换,任务切换或者进程上下文切换。
所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以分为三个部分:
- 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
- 寄存器上下文:通用寄存器、程序寄存器(PC)、处理器状态寄存器(CPSR)、栈指针(SP);
- 系统级上下文:进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈;
当发生进程调度时,操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。
而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。
3.3 进程调度方式
进程调度整体上可以分为两种:
- 非抢占方式:采用这种调度方式,一旦把CPU分配给某进程后,便让该进程一直执行,直到该进程完成或发生某事件而被阻塞,才再把CPU分配给其他进程,决不允许某进程抢占已经分配出去的CPU。显然它难于满足紧急任务的要求,实时系统中不宜采用这种调度方式;
- 抢占方式:允许调度程序根据某种原则,去停止某个正在执行的进程,将已分配给该进程的CPU,重新分配给另一进程;
抢占的原则有:
- 时间片原则:各进程按时间片运行,当一个时间片用完后,便停止该进程的执行而重新进行调度,即CPU分时技术,依赖分时中断。时间片的长短对系统性能是很关键的,太短进程切换开销大,太长进程看起来不再是并发执行,降低系统的响应能力,Linux单凭经验选择尽可能长且响应时间良好的时间片;
- 优先权原则:当一个进程到来时,如果其优先级比正在执行的进程的优先级高,便停止正在执行的进程,将CPU分配给优先级高的进程,使之执行。Linux中进程的优先级是动态的,调度程序跟踪进程的运行,并周期调整他们的优先级,以避免进程饥饿现象,提升系统吞吐量;
3.4 进程调度时机
调度程序虽然特别重要,但它不过是一个存在于内核空间中的函数而已,并不神秘。Linux的调度程序是一个叫schedule的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。我们先来看在什么情况下要执行调度程序,我们把这种情况叫做调度时机。
Linux调度时机主要有:
- 进程状态的转换的时刻:进程终止、进程睡眠,比如进程调用sleep、exit函数,这些函数会主动调用调度程序进行进程调度;
- 当前进程的时间片用完时,进程的时间片是基于时钟中断来实现的,时钟中断是CPU进行任务调度基础;
- 中断发生在内核态,中断执行完毕后进行抢占调度;
- 进程从中断、异常以及系统调用返回用户态时进行调度;不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call,由这个函数进行调度标志的检测,如果必要,则调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完;
根据抢占调度发生的位置有分为两种:
- 用户抢占:当内核即将返回用户空间时,内核会检查need_resched是否设置,如果设置,则调用scheduled方法,此时将发生用户抢占;一般来说,用户抢占包括以下几种:
- 进程从中断返回用户态;
- 进程从异常以返回用户态;
- 进程从系统调用返回用户态;
- 进程时间片用完;时钟中断处理程序会检查当前进程的时间片,当任务的时间片消耗完,scheduler_tick方法就会设置need_resched标志;
- 内核抢占:的是一个在内核态运行的进程,可能在执行内核函数期间被另外一个进程取代;一般来水,内核抢占包括包括:
- 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权;
- 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权;
linux是抢占式的操作系统,linux2.4的内核支持内核抢占不支持用内核抢占,2.6以上的内核都支持。
更多进程调度相关内容可以参考:Linux用户抢占和内核抢占详解(概念 , 实现和触发时机)--Linux进程的管理与调度(二十)。
3.5 基本调度算法
进程基本调度算法包括以下几种:
- 先来先服务算法(FCFS);
- 时间片轮转算法;
- 短任务优先算法;
- 优先级调度算法;
- 混合调度算法;
Linux操作系统在较新的内核中,使用了Completely Fair Scheduler调度算法。具体可以参考Linux CFS调度算法核心解析。
参考文章
[2]Linux进程空间