目录
文章目录
前言
现代服务器系统,大多采用多线程、多进程与多处理器计算平台的组合,本篇主要研究三者间的关系、存在的性能问题及解决方案。
进程与线程
关于进程与线程的描述最经典莫过于**「进程是资源分配的基本单位,线程是 CPU 调度的基本单位」**。
在早期面向进程设计的计算机系统中,进程不仅拥有并管理着诸如 CPU、RAM、文件描述符与信号处理之类的计算机软硬件平台资源,同时也作为程序的基本执行实体。而在现代面向线程设计的计算机系统中,线程取代了进程作为程序的基本执行实体,进程的定位则更趋于逻辑层面,作为资源和线程的容器,一个进程可以拥有多个且至少拥有一个线程作为它的指令执行体。
基本执行单元从进程到线程的演化,是为了解决创建进程(分配资源)、销毁进程(回收资源)、进程间通信(使用外部共享资源)以及进程间切换(上下文数据内存地址空间的转移)等操作的高耗低效问题。简单来说就是进程太 “重(拥有太多资源)” 了,导致了程序的并发性能差。
进程实际是某特定应用程序的一个运行实体。在 Linux 系统中,能够同时运行多个进程,Linux 通过在短的时间间隔内轮流运行这些进程而实现 “多任务”。这一短的时间间隔称为 “时间片”,让进程轮流运行的方法称为 “进程调度” ,完成调度的程序称为调度程序。
通过多任务机制,每个进程可认为只有自己独占计算机,从而简化程序的编写。每个进程有自己单独的地址空间,并且只能由这一进程访问,这样,操作系统避免了进程之间的互相干扰以及 “坏” 程序对系统可能造成的危害。为了完成某特定任务,有时需要综合两个程序的功能,例如一个程序输出文本,而另一个程序对文本进行排序。为此,操作系统还提供进程间的通讯机制来帮助完成这样的任务。Linux 中常见的进程间通讯机制有信号、管道、共享内存、信号量和套接字等。
内核通过 SCI(系统调用接口)提供了一个应用程序编程接口(API)来创建一个新进程(fork、exec 或 Portable Operating System Interface [POSⅨ] 函数),停止进程(kill、exit),并在它们之间进行通信和同步(signal 或者 POSⅨ 机制)。
线程被包含在进程内部,相对于进程的 “重”,线程则追求极致的轻量,除了一些在处理器上执行所必需的资源(e.g. 程序计数器、寄存器和栈)外,不独占任何额外资源,而是线程间共享同一进程的所有资源。所以,无论是创建线程(基本没有资源创建)、销毁线程(基本没有资源回收)、线程间的通信(使用进程内部共享内存地址空间,一个线程生成的数据可以立即用于其他所有线程,线程间的交互可以在不涉及操作系统的情况下完成)或切换(同样得益于共享内存资源)的消耗成本都得到了优化。
需要注意的是,这里并非表示线程一定优于进程,上文只是分别描述了两者的特点而已。线程或进程的最佳应用实践需要建立在应用场景与二者特性是否适配的基础之上,后文中我们会尝试讨论这个问题。
进程与线程的比较:
项目 | 进程 | 线程 |
---|---|---|
定义 | 系统资源分配与调度的基本单位 | 处理器调度的基本单位 |
优点 | 独占操作系统与计算机资源,尤其是独占内存地址空间,所以多进程场景中的单个进程异常不会让整个应用程序崩溃 | 轻量基本不独占资源,所以线程的创建、销毁、通信及切换的成本都更低,更有利于在多线程场景中提高程序的并发性能 |
缺点 | 独占资源多,所以进程的创建、销毁、通信及切换的成本都比较高 | 没有隔离出私有内存,所以单个线程的崩溃可能会导致整个应用程序退出 |
内核线程,用户线程与轻量级进程
Linux 操作系统针对内核态与用户态分别实现了内核线程和用户线程两种线程模型,区分的标准为线程的调度者是在内核态还是在用户态。实际上内核调度的对象是内核线程,用户线程不由内核直接调度,但用户线程最终会映射到内核线程上。多线程通过将内核级资源和用户级资源分离来提供灵活性。
##内核线程
内核线程(KLT,Kernel Level Thread)又称守护进程,由内核负责调度管理。内核线程占用的资源很少,只有内核栈和线程切换时用于保存寄存器数据的的空间。从调度层面来看,内核线程与进程的调度算法比较相似,调度开销也相差不大。但内核线程最大的好处可以其可以通过系统调度器将多个内核线程隐射到不同的处理器核心上,能够更加充分的享受多处理器平台的好处。除此之外,内核线程的创建和销毁开销也是要比进程更少的。举例一些内核线程的特点:
- 内核线程是调度到处理器执行的基本单位,一个内核线程的阻塞不会影响其他内核线程
- 内核会在内核地址空间为每个内核线程都创建线程控制块(TCB),内核根据 TCB 来感知内核线程
- 内核线程可以在全系统内进行资源竞争
- 内核线程间的数据同步效率要比同一进程中线程数据同步的效率低一些
内核线程的优点:
- 在多处理器系统中,内核能够调度同一进程内的多个内核线程并行的在多个处理器上执行
- 如果一个内核线程被阻塞,内核可以调度同一个进程中的另一个内核线程到处理器
内核线程的缺点:内核线程切换的速度已经内核线程间通信的效率较差
轻量级进程
**轻量级进程(LWP,Light Weight Process)**是一个建立在内核之上并由内核支持的用户线程,一个进程内可以包含多个 LWPs,每个 LWP 又会关联至一个特定的内核线程。因此 LWP 是一个独立的线程调度单元,即使有一个 LWP 被阻塞也不会影响到进程中其他 LWP 的执行。在 Linux 操作系统采用的用户线程与内核线程「一对一」映射模型中,LWP 就是用户线程。
LWP 的实现造成了一些局限性:
- LWP 进行系统调用时需要进行模式切换,所以比单纯的系统切换开销更大
- LWP 与内核线程一一对应会消耗内核线程的栈资源(内核地址空间),所以系统不能支持大量的 LWP
用户线程
**用户线程(ULT,User Level Thread)**是在用户态中通过线程库创建的线程,用户线程的创建、调度、销毁和通信都在用户空间完成。内核不会感知到用户线程,内核也不会直接对用户线程进行调度,内核的调度对象依旧是用户进程本身。下面列举一些特性:
- 内核不会为用户线程分配资源,用户线程只在同一进程内竞争资源。
- 用户线程切换由用户程序控制,无需内核干涉,所以没有模式切换的消耗。
- 因为用户线程不被内核感知所以内核也无法将用户线程单独调度到不同的处理器上。用户态中内核只会将进程作为处理器调度的基本单位,同一进程内的多个线程只会在运行进程的处理器上进行线程切换。
- 用户线程不具有独自的线程上下文,因此同一时刻同一进程只能有一个用户线程在运行
用户线程的优点:用户线程切换不进行模式切换,切换开销小,速度快
用户线程的缺点:
- 不能享受多处理器系统的好处同一进程
- 一个用户线程的阻塞将导致整个用户进程内所有用户线程阻塞
- 用户态处理器时间片分配是以用户进程为基本单位的,所以每个线程执行的时间也会相对更少了
轻量级进程与用户线程的区别
LWP 虽然本质上属于用户线程,但 LWP 线程库是建立在内核之上的,其许多操作都要需要进行系统调用,切换开销大,因而并发效率不高;而用户线程则是完全建立在用户空间的线程库,不需要内核参与,因此用户线程切换是即快又低耗的。
为什么 Linux 使用的是 LWP 而不是用户线程?
之前我们提到过 Linux 操作系统中的 LWP 就是 Linux 的用户线程。虽然用户线程即快又低耗,这是舍弃了并发性换来的结果,没有办法充分发挥多处理器系统的价值。对定位于服务器端操作系统的 Linux 而言,并没有采用纯粹的用户线程实现,而是使用 LWP 作为用户线程的替身。所以就 Linux 操作系统而言,用户线程就是 LWP 这句话并不为错。
用户线程与轻量级进程的混合模式
混合模式下的用户线程依旧由建立在用户空间中的用户线程库实现,所以用户线程不会像内核线程一般消耗系统内存地址资源,用户线程可以建议任意多的数量。混合模式的特点在于会使用 LWP 作为影用户线程和内核线程之间的桥梁,多个用户线程对应一个 LWP,一个 LWP 又会映射到一个内核线程中。
这样的关联关系使得用户线程可以利用 LWP 绑定的内核线程作为内核调度单元的特性来实现同一进程中的某个用户线程被阻塞时并不会使其他用户线程也被阻塞。简单来说,混合模式下的用户线程即保留了完全的用户态特性,又解决了内核对用户线程无感导致的并发性问题。
需要注意的是,用户线程和内核线程间插入了 LWP 中间层,其调度的复杂度和调度的开销成正比提升,执行性能受到削弱。混合模式是一种折中的方案。
用户线程和内核线程的区别
-
运行模式
-
用户线程完全运行在用户态
-
内核线程运行在内核态
-
内核支持
-
操作系统内核对用户线程不感知、不调度、不分配资源,所以理论上可以创建任意多的用户线程
-
内核通过 TCB 来感知内核线程,内核线程占用内核栈资源,所以不能运行太多的内核线程
-
内核调度
-
内核线程是内核的调度实体
-
用户线程所属的进程是内核的调度实体
-
处理器分配
-
内核将一个进程调度到一个处理器,进程内的用户线程共享使用该处理器,用户线程不能充分利用多处理器系统
-
内核会将多个内核线程同时调度到不同的处理器上,内核线程可以充分利用多处理器系统
-
系统调用中断
-
用户线程执行系统调用时,会导致其所属进程被中断
-
内核线程执行系统调用时,只导致该线程被中断
线程的实现模型
一对一模型
每个用户线程都映射或绑定到一个内核线程,一旦用户线程终止则内核线程也一同被销毁。Linux 操作系统采用的 LWP 就是一对一模型。
如上图,进程内每个用户线程都可以通过映射到不同的内核线程。
缺点:内核线程数量有限,线程切换会同时涉及到上下文切换和模式切换,开销较大。
多对一模型
多个用户线程映射到一个内核线程,纯粹的用户线程就是多对一模型。
如上图,进程内同一时刻只能有一个用户线程被映射到内核线程。
优点:用户线程切换完全在用户态完成,不涉及模式切换。而且同一进程内的线程切换只需要进行寄存器切换,所以速度很快。
缺点:一个用户线程阻塞,同一进程内的所有线程都被阻塞。
多对多模型
多个用户线程可以映射到少数但不止一个内核线程,是上述两种模型的综合实现,用户线程和 LWP 的混合模式就是多对多模型。
如上图,一个进程中的多个用户线程可以映射到少数但不止一个内核线程。
优点:用户线程的数量依旧没有限制,并且在多处理器系统上会有一定的性能提升。
缺点:性能提升的幅度不及一对一模型
混合线程模型
混合线程模型实现是用户线程和内核线程的交叉,用户线程由运行时库调度器管理,内核线程由操作系统调度器管理,库和系统同时参与线程的调度。
进程拥有自己的内核线程池,进程内准备好执行的用户线程由运行时库分派并标记为 “可用用户线程”,操作系统选择可用用户线程并将它映射到进程内核线程池中的 “可用内核线程”。多个用户线程可以分配给相同的内核线程。
进程与线程调度
Linux 操作系统的用户态依旧延续了进程和线程的概念及描述,从编程的角度来看,我们依旧可以通过进程库或线程库来实现用户态进程或线程的创建与调度。但 Linux 的内核态并没有特别区分进程和线程,线程被视为了一个与其他进程共享某些资源的特殊 “进程”。无论是使用 fork()
来创建进程,还是使用 pthread_create()
创建线程,最终都调用了 do_dork()
来完成 task_struct 结构体的复制。为了方便描述和理解,下文中不时会使用「任务」来作为进程和线程的抽象。
task_struct “任务描述符” 或称 “进程描述符”,包含了单个进程在运行期间所有必要的信息(e.g. PGID 标识了进程组,TGID 标识了线程组,PID 标识了进程或线程),是内核调度的关键。
进程的调度算法
进程调度控制进程对 CPU 的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程。可运行进程实际上是仅等待 CPU 资源的进程,如果某个进程在等待其它资源,则该进程是不可运行进程。Linux 使用了比较简单的基于优先级的进程调度算法选择新的进程。
- 先到先服务(FCFS)调度算法:从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用CPU时再重新调度。
- 短作业优先(SJF)的调度算法:从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 时间片轮转调度算法:时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级反馈队列调度算法:前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
- 优先级调度:为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
进程间的通信方式
- 管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
- 有名管道(Names Pipes): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(FIFO)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
- 消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
- 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
- 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets):此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
进程的生命周期
每个进程都有自己的生命周期,比如创建、执行、终止、删除等。
当用户程序创建一个新进程的时候,父进程会发出一个 fork()
系统调用,然后父进程得到一个新建子进程的 “进程描述符”,并设置一个新的 PID 以及将自己的相关属性复制给子进程。此时父子进程会共享相同的地址空间,直至 exec()
系统调用需要将新的代码复制到子进程的地址空间时,才会为子进程分配新的物理页,子进程再 exec 属于自己的程序代码。这种延迟的数据复制操作称为写时复制(COW,Copy On Write),应用 COW 技术有效避免了不必要的数据复制开销,因为将父进程整个地址空间完全复制给子进程是非常低效且无谓的操作。当子进程执行为程序代码之后,通过 exit()
系统调用终止子进程,系统回收子进程资源并将其状态置为僵尸进程。直至父进程通过 wait()
系统调用得到子进程以及终止了,父进程才会彻底释放子进程的所有数据结构和 “进程描述符”。
进程的状态机
- 创建状态(new):进程正在被创建,尚未到就绪状态。
- 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running):进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
Linux 的线程
上文中我们提到 Linux 的线程实现采用的是一对一模型,作为进程中的执行单元,能够与同一进程中的其他线程并行在不同的处理器中运行。因为线程共享同一进程的资源,比如内存、地址空间、打开的文件等资源,所以需要用户程序实现互斥、锁、序列化等机制来保证共享数据的一致性和数据同步。从性能的角度来看,线程创建的开销要比进程创建更小,因为创建线程时不需要进行资源复制。
LinuxThreads 自 Kernel 2.0 以来成为了 Linux 默认的用户空间线程库,用户线程、LWP、内核线程三者间保持着 1:1:1 的对应关系。前文我们提到内核对 LWP(用户线程)的调度和对进程的调度是类似的,所以在 Linux 中,内核对进程和线程的调度管理并没有十分明确的区分,在调度算法上也有着相似的特征。关于进程调度的细节我们在后文继续讨论。
LinuxThreads 使用一个专门的 “管理线程” 来处理所有线程的管理工作,当进程调用 pthread_create()
创建出第一个线程时会先创建并启动 “管理线程”。后续进程再调用 pthread_create()
创建用户线程时,管理线程通过调用 clone()
来创建用户线程并记录 LWP ID 和子线程 ID 的映射关系。用户线程本质是管理线程的子线程。
线程间的同步的方式
- 斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
- 信号量(Semphares):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 事件(Event):Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级。
进程的优先级
Linux 操作系统也被称为「多任务实时操作系统」,支持多种优先级、调度策略和抢占方式。在用户态,Linux 不会直接调度线程,因为内核对线程是无感知的,所以我们能够看见并使用的大多数操作都是针对进程而言。
以是否具有实时性特征,可以将进程分为实时进程和非实时进程。所谓的实时性就是要求最小的中断延时和任务切换延时,即进程能够不被阻塞或少被阻塞,能够快速的完成响应。实时性在工业领域具有广泛的应用场景和严格要求,对于实时性的需求,Linux 常用的调度算法,无论是 O1 还是 CFS 都难以实现。
所以在设计 Linux 内核的时候,干脆将进程的优先级从逻辑上划分为了实时进程优先级和非实时进程优先级两个平面。优先级在 Linux 内核的对象就是一个数字,由宏 MAX_PRIO 来记录:
- 实时进程优先级:具有 100 个级别对应 MAX_PRIO 的 [0, 99]
- 非实时进程优先级:具有 40 个级别对应 MAX_PRIO 的 [100, 139]
内核通过优先级来确定 CPU 处理的顺序,从 MAX_PRIO 的范围可见,内核调度进程时始终以实时进程为最优先,并且不能被抢占。如果存在已准备的实时进程则优先执行,直到实时进程结束或主动让出 CPU 后,内核才会考虑调度非实时进程。
从操作层面来看,Linux 将实时优先级和非实时优先级分别映射成为了静态优先级和动态优先级。静态优先级设定后是不能够被修改的,较高的静态优先级会拥有更长的时间片。相反,动态优先级是可以被调整的。这是一个合理的设计,毕竟实时代表着最高执行力。
Linux 引入 Nice Level 来改变进程的动态优先级,nice 值的范围是 [-20, 19] 对应 MAX_PRIO 的 [100, 139],默认值为 0,值越小优先级越高。实际上 nice 值是通过公式 PRI(new) = PRI(old) + nice
来决定进程优先级的,静态优先级虽然不能被调整,但却可以通过动态优先级的 nice 值来影响。所以有时候可能你会发现虽然 PA 的 PRI(old) 值比 PB 小,但 PB 却被优先执行了。
需要注意的是,普通用户能否调整的 nice 范围是 [0, 19],并且只能调高不能调低。root 用户才能随意调整 nice 值。
调整进程的动态优先级
设定进程优先级对改善 Linux 多任务环境中的程序执行性能非常有用。
查看进程资源使用信息:
root@devstack-all-in:~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 78088 9188 ? Ss 04:26 0:03 /sbin/init maybe-ubiquity
...
stack 2152 0.1 0.8 304004 138160 ? S 04:27 0:04 nova-apiuWSGI worker 1
stack 2153 0.1 0.8 304004 138212 ? S 04:27 0:04 nova-apiuWSGI worker 2
...
查看进程优先级信息:
root@devstack-all-in:~# ps -le
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1 0 0 80 0 - 19522 ep_pol ? 00:00:03 systemd
1 S 0 2 0 0 80 0 - 0 kthrea ? 00:00:00 kthreadd
1 I 0 4 2 0 60 -20 - 0 worker ? 00:00:00 kworker/0:0H
1 I 0 6 2 0 60 -20 - 0 rescue ? 00:00:00 mm_percpu_wq
...
- UID:进程执行者
- PID:进程代号
- PPID:父进程代号
- PRI:进程优先级,值越小优先级越高
- NI:进程的 nice 值
查看 nice 不为 0 的非实时进程:
[root@localhost ~]# ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,policy,stat,wchan:14,comm|awk '$4 ~ /-/ &&$5 !~/0/ {print $0}'
63 63 TS - 5 14 2 0.0 TS SN ksm_scan_threa ksmd
64 64 TS - 19 0 2 0.0 TS SN khugepaged khugepaged
12995 12995 TS - -4 23 1 0.0 TS S<sl ep_poll auditd
nice 指令:执行命令的同时设定 nice 值。e.g.
nice -n -5 service httpd start
renice 指令:修改已经存在的非实时进程的 nice 值。e.g.
[root@localhost ~]# ps -le | grep nova-compute
4 S 1000 9301 1 2 80 0 - 530107 ep_pol ? 00:02:50 nova-compute
[root@localhost ~]# renice -10 9301
9301 (process ID) old priority 0, new priority -10
[root@localhost ~]# ps -le | grep nova-compute
4 S 1000 9301 1 2 70 -10 - 530107 ep_pol ? 00:02:54 nova-compute
设定实时进程优先级
查看系统中所有的实时进程:
[root@localhost ~]# ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,policy,stat,wchan:14,comm |awk '$4 !~ /-/{print $0}'
PID TID CLS RTPRIO NI PRI PSR %CPU POL STAT WCHAN COMMAND
7 7 FF 99 - 139 0 0.0 FF S smpboot_thread migration/0
10 10 FF 99 - 139 0 0.0 FF S smpboot_thread watchdog/0
11 11 FF 99 - 139 1 0.0 FF S smpboot_thread watchdog/1
12 12 FF 99 - 139 1 0.0 FF S smpboot_thread migration/1
chrt 指令可以显示、设定实时进程的静态优先级以及修改实时进程的调度策略。
修改进程的静态优先级:chrt -p [1..99] {pid}
[root@localhost ~]# ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,policy,stat,wchan:14,comm |awk '$4 !~ /-/{print $0}'
PID TID CLS RTPRIO NI PRI PSR %CPU POL STAT WCHAN COMMAND
27 27 FF 99 - 139 4 0.0 FF S smpboot_thread migration/4
[root@localhost ~]# chrt -p 31
pid 31's current scheduling policy: SCHED_FIFO
pid 31's current scheduling priority: 99
[root@localhost ~]# chrt -f -p 50 31
[root@localhost ~]# chrt -p 31
pid 31's current scheduling policy: SCHED_FIFO
pid 31's current scheduling priority: 50
查看进程运行状态及其内核函数名称:
[root@localhost ~]# ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,policy,stat,wchan:34,nwchan,pcpu,comm
PID TID CLS RTPRIO NI PRI PSR %CPU POL STAT WCHAN WCHAN %CPU COMMAND
1 1 TS - 0 19 4 0.0 TS Ssl ep_poll ffffff 0.0 systemd
2 2 TS - 0 19 0 0.0 TS S kthreadd b1066 0.0 kthreadd
3 3 TS - 0 19 0 0.0 TS S smpboot_thread_fn b905d 0.0 ksoftirqd/0
...
44 44 TS - 0 19 7 0.0 TS R - - 0.0 kworker/7:0
- wchan:显示进程处于休眠状态的内核函数名称,如果进程正在运行则为
-
,如果进程具有多线程且ps
指令未显示,则为*
。 - nwchan:显示进程处于休眠状态的内核函数地址,正在运行的任务将在此列中显示短划线
-
。
进程的调度
在多任务多处理器计算机系统中必须提供一种方法,让多个进程之间尽可能公平地共享处理器等资源,同时还要考虑到不同进程的任务优先级。
Linux 内核实现了调度器来解决这一问题,调度器主要职责是保证处理器都处于忙碌的状态,决定了运行线程的处理器及时间片长度。但需要注意的是,调度器并不负责保证用户程序的执行性能。
调度类型
高级调度(作业调度):根据作业调度算法从外存后备队列将作业调入内存,并分配资源、创建作业相应的进程。作业完成后也做一些善后工作,例如:关闭文件等。
中级调度(平衡调度):涉及进程在内外存之间的交换,当主存资源紧缺时,会将暂不运行的进程从内存调至外存,此时进程处于 “挂起” 状态。当进程又具备了运行条件且主存资源充裕时,再将进程从外存调至内存。中级调度的主要目的是提高内存利用率和系统吞吐量。
低级调度(进程/线程调度):根据调度策略从处理器的就绪队列中选择一个进程或线程让它获取处理器的使用权。
-
非剥夺式(非抢占式)调度:调度程序一旦把处理器分配给某个进程/线程后,就会一直占用处理器直到执行完成或主动让出时,才会将处理器分配给其他进程/线程。适用于批处理系统。
-
剥夺式(抢占式)调度:当一个进程/线程使用处理器时,调度策略会根据某种规则将处理器分配给其他进程/线程。适用于分时系统和实时系统。
Linux 的进程/线程调度策略
实时调度策略:
-
SCHED_FIFO:先到先服务调度策略,相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务。当前线程占用处理器直到它阻断、退出或被更高的线程抢占为止。
-
SCHED_RR:时间片轮转调度策略,采用时间片,相同优先级的任务当用完时间片后会被放到队列尾部。同样,高优先级的任务可以抢占低优先级的任务。常用于需要以相同优先级运行多个任务的场景。
-
SCHED_DEADLINE:针对突发型计算,且适用于对延迟和完成时间高度敏感的任务。基于Earliest Deadline First (EDF) 调度算法。
非实时调度策略:
-
SCHED_NORMAL:普通进程调度策略,通过 CFS 调度器实现。
-
SCHED_BATCH:采用分时策略,根据动态优先级(nice 值)来分配处理器运算资源。适用于非交互的处理器消耗型进程。
-
SCHED_IDLE:优先级最低,在系统空闲时才跑这类进程。适用于系统负载很低的时候。
修改进程的调度策略
chrt 指令支持 6 种调度器策略。e.g.
root@devstack-all-in:~# chrt --help
Show or change the real-time scheduling attributes of a process.
Set policy:
chrt [options] <priority> <command> [<arg>...]
chrt [options] --pid <priority> <pid>
Get policy:
chrt [options] -p <pid>
Policy options:
-b, --batch set policy to SCHED_BATCH
-d, --deadline set policy to SCHED_DEADLINE
-f, --fifo set policy to SCHED_FIFO
-i, --idle set policy to SCHED_IDLE
-o, --other set policy to SCHED_OTHER
-r, --rr set policy to SCHED_RR (default)
设定进程的调度策略:
root@devstack-all-in:~# chrt -f 10 bash
root@devstack-all-in:~# chrt -p $$
pid 6344's current scheduling policy: SCHED_FIFO
pid 6344's current scheduling priority: 10
- SCHED_FIFO:先到先服务调度策略。一旦处于可执行状态就会一直执行,直到它自己阻塞或者释放 CPU。只能被优先级更高的进程抢占,一般用于延时要求较短的进程,被赋予较高的优先级。
[root@localhost ~]# chrt -r 10 bash
[root@localhost ~]# chrt -p $$
pid 13360's current scheduling policy: SCHED_RR
pid 13360's current scheduling priority: 10
[root@localhost ~]# ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,policy,stat,wchan:14,comm |awk '$4 !~ /-/{print $0}'
PID TID CLS RTPRIO NI PRI PSR %CPU POL STAT WCHAN COMMAND
13360 13360 RR 10 - 50 7 0.0 RR S do_wait bash
- SCHED_RR:时间片轮转调度策略。进程执行直到时间片用完或者自己阻塞和释放CPU。只能被优先级更高的进程抢占,一般用于延时要求稍长的进程,被赋予较低的优先级。
线程切换的性能消耗
直接开销:由线程切换本身引起的开销。
-
上下文切换:线程执行现场(task_struct 结构体、寄存器、程序计数器、线程栈等)的保留和载入。
-
运行模式切换:线程切换只能在内核态完成,如果当前线程处于用户态,则必然需要先将线程从用户态切换为内核态。
-
调度器负载:调度器负责线程状态的管理与调度,如果存在优先级调度,则还需要维护线程的优先级队列。当线程切换比较频繁,那么调度器的负载成本也会比较大。
间接开销:是直接开销的副作用。包括在多核 Cache 之间的共享数据
-
高速缓存缺失(Cache Missing):新旧线程切换,如果二者访问的地址空间不接近,则会引起缓存缺失(缓存命中率低,还要花费额外的时间来不断刷新)。具体影响范围取决于计算机系统的实现,处理器体系结构和用户程序的代码实现。如果系统的缓存较大,则能减缓缓存缺失的影响,如果二者访问的地址空间比较接近,也能够降低缓存缺失的概率。
-
多核缓存共享数据同步:同一进程的不同线程在多个处理器上运行,如果这些线程间存在共享数据,同时这些数据又存在缓存中。那么当另一个线程在新的处理器上运行时,就需要同步其他处理器的缓存数据到新处理器缓存中。
如何减少上下文切换?
- 如果是让步式上下文切换,线程会主动释放处理器。可通过减少锁竞争来避免上下文切换。
- 如果是抢占式上下文切换,线程会因用尽时间片而放弃处理器或被其他优先级更高的线程抢占处理器。可通过适当减少线程数来避免上下文切换。
使用 vmstat 指令查看当前系统的上下文切换情况:
root@devstack:~# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
4 1 0 4505784 313592 7224876 0 0 0 23 1 2 2 1 94 3 0
- r:CPU 运行队列的长度和正在运行的线程数
- b:正在阻塞的进程数
- swpd:虚拟内存已使用的大小,如果大于 0,表示机器的物理内存不足了。如果不是程序内存泄露的原因,那么就应该升级内存或者把耗内存的任务迁移到其他机器上了
- si:每秒从磁盘读入虚拟内存的大小,如果大于 0,表示物理内存不足或存在内存泄露,应该杀掉或迁移耗内存大的进程
- so:每秒虚拟内存写入磁盘的大小,如果大于 0,同上
- bi:块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是 1024Byte
- bo:块设备每秒发送的块数量,例如读取文件时,bo 就会大于 0。bi 和 bo 一般都要接近 0,不然就是 I/O 过于频繁,需要调整
- in:每秒 CPU 中断的次数,包括时间中断
- cs:每秒上下文切换的次数,这个值要越小越好,太大了,要考虑减少线程或者进程的数目。上下文切换次数过多表示 CPU 的大部分时间都浪费在上下文切换了而不是在执行任务
- st:CPU 在虚拟化环境上在其他租户上的开销
查看进程使用 CPU 的统计信息:
root@devstack:~# pidstat -p 12285
Linux 4.4.0-91-generic (devstack) 07/15/2018 _x86_64_ (8 CPU)
02:53:02 PM UID PID %usr %system %guest %CPU CPU Command
02:53:02 PM 0 12285 0.00 0.00 0.00 0.00 5 python
- PID:进程标识
- %usr:进程在用户态运行所占 CPU 的时间比率
- %system:进程在内核态运行所占 CPU 的时间比率
- %CPU:进程运行所占 CPU 的时间比率
- CPU:进程在哪个核上运行
- Command:创建进程对应的命令
相关阅读: