深入理解Linux内核:进程调度机制原理

发布于:2025-03-10 ⋅ 阅读:(28) ⋅ 点赞:(0)

探讨 Linux 进程调度之前,让我们先来认识一下这个在操作系统中起着关键作用的重要机制。进程调度就像是操作系统的指挥家,精心安排着各个进程对 CPU 资源的使用,确保系统高效、稳定地运行。在 Linux 系统中,进程调度更是其核心功能之一,它决定着众多任务如何有序地执行,以及如何在不同的应用场景下实现资源的合理分配。今天,我们就一同走进 Linux 进程调度的神秘世界,去揭开它的面纱,了解它的工作原理和重要意义。

一、进程调度简介

1.1概述

Linux内核中,进程(Process)是最基本的执行实体,它代表了正在执行的程序的实例。进程调度在操作系统中处于核心地位,是操作系统实现多任务处理的关键机制。它就如同乐队的指挥,确保各个进程在有限的 CPU 资源下有序地执行。

图片

在现代计算机系统中,往往有多个进程同时竞争 CPU 资源。如果没有有效的进程调度,系统可能会陷入混乱,导致某些进程长时间占用 CPU,而其他进程无法得到执行机会。例如,在一个没有进程调度的系统中,一个大型计算任务可能会一直占用 CPU,使得其他诸如网页浏览、文档编辑等轻量级任务无法执行,严重影响用户体验。

进程调度的高效性直接影响着系统的性能。一个高效的进程调度算法能够在短时间内完成大量的进程切换,使得 CPU 资源得到充分利用。以轮转调度(RR)算法为例,它将 CPU 时间分成若干时间片,每个进程轮流执行一个时间片。这样可以确保每个进程都能得到一定的执行时间,从而提高系统的整体吞吐量。据统计,在合理设置时间片的情况下,轮转调度算法可以使系统的吞吐量提高 20% 至 30%。

公平性也是进程调度的重要目标之一。每个进程都应该有机会获得合理的 CPU 时间片,避免饥饿现象的发生。例如,优先级调度算法中,如果源源不断地产生高优先级的进程,那么低优先级的进程可能会长时间得不到执行。为了解决这个问题,可以采用老化等技术,逐渐增加等待很长时间的进程的优先级。

⑴在Linux内核进程调度学习过程中,需要区分几个比较重要的概念:

轻量级进程定义: LWP是一种内核支持的用户线程实现,每一个LWP都对应着内核中的一个实体,也就是说,每个LWP都有自己的内核级线程支持,从而能够独立地被内核调度。LWP结合了用户线程和内核线程的优点,既可以享受到多线程的优势,又能避免传统用户线程的全局阻塞问题。

内核线程定义: 内核线程是直接在内核空间运行的线程,它没有独立的用户空间,主要执行内核任务,不与任何特定的用户进程关联。内核线程通常用于执行内核维护工作,如定时器中断处理、I/O调度、垃圾回收等后台服务。

特点: 没有自己的地址空间,所有内核线程共享内核地址空间,可以直接访问硬件资源,但不能执行用户态的代码。

⑵用户进程和用户线程

用户进程:用户进程是运行在用户空间的应用程序实例,它拥有独立的地址空间、打开的文件描述符集合以及其他系统资源。一个用户进程可以包含一个或多个线程(无论是内核线程还是用户线程/LWP)。

用户线程:用户线程是在用户空间创建和管理的线程,存在于进程的地址空间内部。用户线程由进程自己或用户空间的线程库(如POSIX Pthreads)创建和调度,而非由操作系统内核直接管理。用户线程依赖于用户态的线程库实现上下文切换,速度相对较慢。早期的用户线程在没有内核支持的情况下,如果其中一个线程阻塞在系统调用上,会导致整个进程阻塞。

⑶轻量级进程和用户线程的关系

在Linux系统中,当你使用用户层的线程库(如POSIX Pthreads)创建用户线程时,大多数情况下(特别是使用Native POSIX Thread Library,NPTL时),操作系统会在内核层面对应地创建一个内核线程。NPTL实现了用户线程与内核线程的1:1映射关系,意味着每个用户线程都有一个与之紧密耦合的内核线程。

内核通过维护内核线程来确保线程的调度、上下文切换、系统调用响应等功能。这样一来,当用户线程执行系统调用或发生阻塞时,内核能够透明地调度另一个线程继续执行,避免了用户级线程模型可能导致的整个进程被阻塞的问题。此外,由于内核直接参与调度,还能保证线程在多处理器环境下的公平性和高效性。

1.2进程查看命令

⑴ps (Process Status)命令是Linux及类Unix系统中最基础的进程查看工具之一,它提供了当前系统中进程状态的一次性快照视图。通过不同的选项,您可以获取到不同级别的进程信息。

以下是一些常用选项及其作用:

  • -e 或 --every:显示系统中所有的进程。

  • -f 或 --full:提供完整的格式输出,包括进程树状关系和环境变量等额外信息。

  • -l 或 --long:长格式输出,包含更多详细信息,如F旗表示进程正在等待文件锁。

  • -u 或 --user:按照用户来显示进程,并显示每个进程的CPU和内存使用情况。

  • -aux 是一个常见的组合选项,用于显示系统中所有用户的全部进程,包括后台进程(不与终端关联的进程)。

例如,ps -ef 将显示出当前系统中所有进程的详细信息,包括PID(进程ID)、PPID(父进程ID)、TTY(终端类型)、CWD(当前工作目录)、CMD(启动命令)等字段。

⑵相比之下,top命令则提供了一个动态实时的视图,它可以持续不断地刷新并显示当前系统中各进程的资源使用情况。启动top命令后,您将看到一个全屏界面,其中包括:

  • 进程列表:按照默认排序(通常是CPU使用率或优先级)列出正在运行的进程及其相关信息,如PID、USER(执行进程的用户)、PR(优先级)、NI(nice值,影响优先级)、VIRT(虚拟内存大小)、RES(常驻内存大小)、%CPU和%MEM(CPU和内存使用百分比)等。

  • 系统总体状态:包括系统运行时间、登录用户数、系统负载、CPU和内存的整体使用状况等统计数据。

  • 交互式操作:在top运行过程中,用户可以通过键盘输入相应的命令(如按P键切换到按CPU使用率排序,按M键切换到按内存使用率排序,或使用k键杀死指定进程等)来进行进一步的进程管理和监控。

1.3进程程的几个要素

  • 有一段程序待其执行

  • 有进程专用的系统堆栈空间

  • 在内核有task_struct结构体

  • 进程有独立的存储空间,拥有专用的用户空间

如果具备前面三条而缺少第4条就可以称为线程“”,如果完全没有用户空间,就称为“内核线程 ”,如果共享用户空间就称为“用户线程” 。

二、进程的生命周期

2.1进程状态文字描述

Linux操作系统属于多任务操作系统,系统中的每个进程能够分时复用CPU时间片,通过有效的进程调度策略实现多任务并行执行。而进程在被CPU调度运行,等待CPU资源分配以及等待外部事件时会属于不同的状态。

进程状态如下:

  • 创建状态:新进程刚刚被创建,尚未开始执行。

  • 就绪状态:进程已准备好所有必需资源,等待CPU分配时间片执行。

  • 执行状态:进程已获得CPU资源并在其中运行。

  • 阻塞状态:进程因等待某个资源或事件而暂时停止运行,从CPU队列中移除。

  • 终止状态:进程已完成执行或被终止,不再存在。

以上阶段可以通过调度程序进行管理,使得多个进程能够高效地共享CPU资源。整个过程在操作系统内核中处理,确保系统稳定性和性能

进程状态程序中的体现:

#define TASK_RUNNING			0x00000000
#define TASK_INTERRUPTIBLE		0x00000001
#define TASK_UNINTERRUPTIBLE	0x00000002
#define __TASK_STOPPED			0x00000004
#define __TASK_TRACED			0x00000008
  • TASK_RUNNING 表示进程处于可运行状态。这意味着进程已经准备好在CPU上执行,并且调度器可以选择它来进行运行。当进程获取到CPU时间片时,它就会进入运行状态。

  • TASK_INTERRUPTIBLE 表示进程处于可中断睡眠状态。这种状态下,进程正在等待某个事件发生(例如I/O操作完成、锁可用等),并且如果收到信号或者等待的条件满足,它可以被唤醒并重新加入到可运行队列中。在可中断睡眠期间,进程可以响应信号并改变其状态。

  • TASK_UNINTERRUPTIBLE 表示进程处于不可中断睡眠状态。类似可中断睡眠,进程同样在等待某种资源或事件,但是在此状态下,进程不会响应任何信号,即使接收到信号也不会立即醒来,除非等待的资源变为可用或特定条件达成。

  • __TASK_STOPPED 标志意味着进程已停止执行,通常是因为收到了SIGSTOP或SIGTSTP这样的停止信号,或者是调试器暂停了进程。停止的进程不会消耗CPU资源,直到收到SIGCONT信号恢复执行。

  • __TASK_TRACED 表示进程正在被调试器或其他跟踪工具追踪,并进入了跟踪停止状态。在这种状态下,进程同样不会执行,等待调试器的进一步操作,比如单步执行、继续执行等。

这些状态标志会被组合在一个进程控制块(PCB,在Linux内核中表现为task_struct结构体的一个成员变量state)中,以表示进程的当前状态。调度器根据这些状态决定何时何地将进程投入运行或从运行状态移除。在实际的内核源码中,为了准确反映进程状态,这些宏可能会与其他标志位一起使用或组合起来形成更复杂的状态标识。

2.2进程状态的切换

如下图,便是进程进行状态之间的切换,这些工作都是有调度器来完成的。

图片

2.3task_struct数据结构

进程是操作系统调度的一个实体,需要对进程所必须资源做一个抽象化,此抽象化为进程控制块 (PCB,Process Control BLock) ,PCB在Linux内核里面采用task_struct结构体来描述进程控制块。Linux内核涉及进程和程序的所有算法都围绕名为task_struct的数据结构而建立操作。具体Linux内核源码task_struct结构体核心成员如下(task_struct结构体过于庞大,暂时了解几个重要成员)task_struct定义在include\linux\sched.h:

  • __state:表示当前进程状态,例如可运行、睡眠、僵死等。

  • stack:指向进程的内核栈。

  • usage:引用计数,用于跟踪进程使用情况。

  • prio、static_prio和normal_prio:描述进程的调度优先级和策略。

  • se、rt和dl:分别对应CFS(完全公平调度器)、实时调度和Deadline调度的调度实体。

  • mm:指向进程的内存描述符结构(mm_struct),管理进程的虚拟内存。

  • active_mm:在没有独立内存空间时,指向当前活动的内存描述符。

  • exit_state、exit_code和exit_signal:进程退出时的状态、退出码和发送给父进程的信号。

  • pid和tgid:分别代表进程ID和线程组ID。

  • real_parent、parent、children和sibling:用于构建进程间的父子、兄弟关系,形成进程树。

  • files:指向进程打开的文件表,即files_struct结构体,记录所有已打开的文件描述符。

  • signal和sighand:管理和处理进程接收到的信号。

  • blocked、real_blocked和saved_sigmask:记录进程当前屏蔽的信号集合。

  • nsproxy:命名空间代理,用于管理和切换不同命名空间。

  • fs:指向文件系统信息结构,记录进程的当前工作目录、根目录等文件系统相关信息。

其他字段还包括了进程的调度统计信息、时间统计、内存页面错误统计、POSIX定时器、安全特性、审计信息等。

需要注意的成员:内存块指针,特殊的是对于内核线程而言的mm是空指针,active_mm是内核线程在运行的时候向进程借用的地址空间。

struct mm_struct		*mm;
struct mm_struct		*active_mm;

2.4进程优先级

⑴优先级的代码表示

描述进程的调度优先级和策略,之后的任务调度以及时间片分配都要用到优先级:

	int				prio;
	int				static_prio;
	int				normal_prio;
	unsigned int	rt_priority;
  • int prio: 这个字段代表进程的动态优先级,它是根据进程的行为和系统负载动态调整的。在传统的Linux调度器(如CFS调度器)中,这个优先级通常被映射到调度实体(sched_entity)的一个虚拟运行时间(vruntime),而不是一个直观意义上的数字大小,较大的vruntime意味着较低的优先级。

  • int static_prio: 静态优先级,也称为nice值,在Linux中范围是-20至19,数值越小表示优先级越高。静态优先级可以通过nice值或者用户权限改变,但不会像动态优先级那样频繁变化。

  • int normal_prio: 此字段在某些Linux调度器实现中可能用来表示经过nice值调整后的正常优先级,它结合了静态优先级和可能的额外优先级调整因素。

  • unsigned int rt_priority: 实时优先级,仅适用于实时调度策略(如SCHED_FIFO或SCHED_RR)。实时进程有固定的优先级分配,rt_priority值越大,表示进程的实时优先级越高,抢占其他进程的可能性也就越大。实时进程一般不受nice值的影响,其优先级高于普通进程。在实时调度策略下,rt_priority用于确定进程在实时进程队列中的相对位置。

⑵Linux内核下的进程分类

在Linux内核中,进程可以按照其调度需求和优先级的不同分为不同的类别,主要包括:

★普通进程(Normal Process):又称为分时进程,这类进程在Linux系统中遵循默认的分时调度策略,如CFS(Completely Fair Scheduler)。它们按照各自权重(nice值)和虚拟运行时间(vruntime)来获取CPU时间片。nice值可以在[-20, 19]范围内调整,数值越小,优先级越高,但总体来说,普通进程之间是公平共享CPU资源的。

★实时进程(Real-time Process):实时进程在满足特定条件的情况下需要得到及时响应,具有更高的优先级。Linux内核提供两种实时调度策略:SCHED_FIFO(先进先出)和SCHED_RR(轮转调度)。

  • SCHED_FIFO:实时进程中,优先级高的进程总是优先执行,一旦开始运行,除非进程主动放弃CPU(如阻塞等待I/O或睡眠),否则不会被优先级相同或更低的其他进程抢占。

  • SCHED_RR:同样是实时进程,但它在用完时间片后会重新加入队列等待下一次调度,这样可以保证在相同优先级的实时进程中实现时间片轮转。

★限期进程(Deadline Process):在一些文献和系统中,也可能提到限期进程这一概念,它指的是那些具有严格截止时间要求的任务,必须在规定时间内完成。在Linux内核的标准调度器中并没有直接的限期调度策略,但在实时扩展(如PREEMPT_RT补丁集)的支持下,可以通过特殊的实时调度策略或者其他方法模拟实现这种功能。实际应用中,这种类型的进程通常归入实时进程范畴,通过设定合适的实时优先级并配合调度算法确保其能够在截止时间前完成计算。

⑶优先级的在不同类型进程的分配

  • 限期进程的优先级是-1;

  • 实时进程的优先级1-99,优先级数值最大,表示优先级越高;

  • 普通进程的静态优先级为: 100-139,优先级数值越小,表示优先级越高,可通过修改nice值改变普通进程的优先级,优先级等于120加 上nice值;

限期进程的优先级比实时进程要高,实时进程的优先级比普通进程要高下表就是描述了不同进程对应的优先级成员的变化:

图片

2.5进程调度的重要性

⑴提升系统性能

进程调度对系统性能的提升起着关键作用。通过合理地分配 CPU 资源,进程调度可以极大地提高 CPU 利用率。例如,在完全公平调度器(CFS)中,根据进程的虚拟运行时间来分配 CPU 时间,确保每个进程都能获得相对公平的执行机会,从而有效提高 CPU 的利用率。据统计,采用 CFS 的系统中,CPU 利用率可以提高 15% 至 20%。

系统吞吐量是衡量系统性能的另一个重要指标。良好的进程调度算法可以在单位时间内完成更多的进程,从而提高系统吞吐量。以多级反馈队列调度为例,它将就绪队列分成多个优先级不同的队列,每个队列采用不同的调度算法。短作业可以在高优先级队列中快速得到执行,而长作业则在低优先级队列中逐步执行,这样可以兼顾不同类型进程的需求,提高系统的整体吞吐量。实验表明,使用多级反馈队列调度的系统,吞吐量可以比传统的先来先服务调度提高 30% 至 40%。

此外,进程调度还可以降低周转时间、等待时间和响应时间。周转时间是指进程从提交到完成所花费的时间,等待时间是进程在就绪队列中等待的时间,响应时间是从进程提交到首次获得 CPU 时间的时间间隔。通过合理的调度算法,如短作业优先调度,可以优先执行短作业,减少这些时间指标。研究显示,在特定的工作负载下,短作业优先调度可以将平均周转时间降低 20% 至 30%,响应时间降低 15% 至 20%。

⑵确保公平性

公平性是进程调度的核心目标之一。为了避免进程饥饿现象,各种调度算法都采取了不同的措施。在优先级调度中,虽然高优先级的进程会优先获得 CPU 资源,但为了防止低优先级进程长时间得不到执行,可以采用动态调整优先级的方法。例如,随着低优先级进程的等待时间增加,逐渐提高其优先级,使其有机会获得 CPU 执行时间。

同时,一些调度算法还通过限制高优先级进程的执行时间来确保公平性。例如,在实时调度中,硬实时任务虽然要求在严格的时间限制内完成,但也不能无限占用 CPU 资源。调度算法会在保证硬实时任务按时完成的前提下,合理分配 CPU 时间给其他进程,以实现系统的整体公平性。

三、进程系统调用

3.1系统调用函数

当运行应用程序的时候,调用fork()/vfork()/clone()函数就是系统调用。系统调用就是应用程序如何进入内核空间执行任务,程序使用系统调用执行一系列的操作: 比如创建进程、文件IO等等。

系统调用框图(使用Linux版本为6.1的内核,不同的内核其系统调用有点差异) 如下所示:

图片

⑴fork系统调用代码

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
	struct kernel_clone_args args = {
		.exit_signal = SIGCHLD,
	};
 
	return kernel_clone(&args);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}

⑵vfork系统调用代码

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
	struct kernel_clone_args args = {
		.flags		= CLONE_VFORK | CLONE_VM,
		.exit_signal	= SIGCHLD,
	};
 
	return kernel_clone(&args);
}
#endif

⑶clone系统调用代码

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 unsigned long, tls,
		 int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
		int, stack_size,
		int __user *, parent_tidptr,
		int __user *, child_tidptr,
		unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 unsigned long, tls)
#endif
{
	struct kernel_clone_args args = {
		.flags		= (lower_32_bits(clone_flags) & ~CSIGNAL),
		.pidfd		= parent_tidptr,
		.child_tid	= child_tidptr,
		.parent_tid	= parent_tidptr,
		.exit_signal	= (lower_32_bits(clone_flags) & CSIGNAL),
		.stack		= newsp,
		.tls		= tls,
	};
 
	return kernel_clone(&args);
}
#endif

⑷进程退出

①、进程主动终止: 从main()函数返回,链接程序会自动添加到exit()系统调用; exit系统调用在内核定义如下\kernel\exit.c:

SYSCALL_DEFINE1(exit, int, error_code)
{
	do_exit((error_code&0xff)<<8);
}

②、进程被动终止: 进程收到一个自己不能处理的信号;进程收到 SIGKILL等终止信息。

⑸内核线程

定义:它是独立运行在内核空间的进程,与普通用户进程区别在于内核线程没有独立的进程地址空间。task_struct数据结构里面有一个成员指针mm设置为NULL,它只能运行在内核空间。内核创建一个内核线程代码体现如下:

/*
 * Create a kernel thread.
 */
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
	struct kernel_clone_args args = {
		.flags		= ((lower_32_bits(flags) | CLONE_VM |
				    CLONE_UNTRACED) & ~CSIGNAL),
		.exit_signal	= (lower_32_bits(flags) & CSIGNAL),
		.fn		= fn,
		.fn_arg		= arg,
		.kthread	= 1,
	};
 
	return kernel_clone(&args);
}

3.2常见的进程调度算法

⑴先来先服务(FCFS)

先来先服务调度算法是一种最简单的调度算法,它按照进程到达的先后顺序进行调度。当一个进程进入就绪队列时,它会按照到达的顺序排队等待 CPU 的分配。这种算法的优点是实现简单,公平性较高,每个进程都按照其到达的顺序依次获得 CPU 时间。然而,它也存在明显的缺点。对于长进程来说,可能会长时间占用 CPU,导致后续到达的短进程和 I/O 繁忙型作业等待时间过长。例如,假设有三个进程 P1、P2 和 P3,P1 的执行时间为 30 秒,P2 的执行时间为 5 秒,P3 的执行时间为 20 秒。如果按照先来先服务的算法调度,P1 先执行,那么 P2 和 P3 就需要等待 30 秒后才能开始执行,这大大增加了短进程和 I/O 繁忙型作业的等待时间,降低了系统的整体效率。

⑵短作业优先(SJF)

短作业优先调度算法优先调度执行时间最短的进程。这种算法的目的是减少平均等待时间,提高系统的吞吐量。然而,它也存在一些问题。首先,它可能导致长作业饥饿,因为长作业可能一直等待短作业执行完毕后才能获得 CPU 时间。其次,准确估计作业的执行时间是非常困难的。在实际应用中,程序员很难准确估计作业的执行时间,通常会偏长估计,这可能导致算法的效果不如预期。例如,如果有一个长作业需要执行 100 秒,而不断有短作业到来,那么长作业可能永远也得不到调度,从而出现饥饿现象。

⑶优先级调度

优先级调度算法根据进程的优先级进行调度。每个进程都被赋予一个优先级,优先级高的进程优先获得 CPU 时间。这种算法可以根据进程的重要性或紧急程度来分配 CPU 资源,具有一定的灵活性。但是,它也可能导致低优先级进程饥饿。如果不断有高优先级进程到来,低优先级进程可能长时间得不到执行。静态优先级调度在进程创建时分配优先级,并在整个执行过程中保持不变;动态优先级调度则根据进程的行为和状态动态调整优先级。例如,在一些实时系统中,紧急任务被赋予高优先级,以确保它们能够及时得到处理。但是,如果高优先级任务过多,低优先级任务可能会被长时间忽略。

⑷轮转调度(RR)

轮转调度算法将 CPU 时间分成固定大小的时间片,所有进程轮流获得一个时间片的 CPU 使用权。这种算法具有较好的公平性,每个进程都能在一定时间内获得 CPU 时间。然而,时间片的大小对系统性能有很大影响。如果时间片太小,会导致进程切换频繁,增加系统开销;如果时间片太大,轮转调度算法就会退化为先来先服务算法。此外,轮转调度算法不利于处理紧急作业,因为每个进程都需要等待轮到自己才能获得 CPU 时间。例如,假设有一个紧急任务需要立即执行,但按照轮转调度算法,它可能需要等待很长时间才能获得 CPU 时间。

⑸多级反馈队列调度

多级反馈队列调度算法结合了多种调度策略,根据进程的特性将其分配到不同的队列中,每个队列采用不同的调度算法。这种算法具有较高的灵活性,可以适应不同类型的进程。例如,高优先级的短作业可以分配到高优先级队列中,采用短作业优先调度算法;长作业可以分配到低优先级队列中,采用先来先服务调度算法。然而,这种算法相对复杂,需要维护多个队列,增加了系统的开销和管理难度。

⑹高响应比优先调度算法

高响应比优先调度算法权衡了短作业和长作业,兼顾了等待时间和执行时间。响应比是等待时间与执行时间的比值,响应比高的进程优先获得 CPU 时间。这种算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。但是,计算响应比会增加系统开销,因为每次调度都需要计算每个进程的响应比。例如,在一个有多个进程等待调度的系统中,计算响应比需要消耗一定的时间和计算资源。

四、进程调度的实现与优化

图片

4.1调度算法的选择

不同的场景和需求对进程调度算法有着不同的要求。在批处理系统中,主要追求高吞吐量和系统资源的充分利用。例如,短作业优先算法可以在一定程度上提高批处理系统的效率,因为它优先处理执行时间短的作业,从而在单位时间内可以完成更多的作业。据统计,在一些大型数据处理中心采用短作业优先算法,系统吞吐量可以提高 15% 至 20%。

对于交互式系统,响应时间是关键指标。此时,轮转调度算法可能更为合适,因为它可以确保每个进程都能在较短的时间内获得 CPU 时间,从而提高系统的响应速度。例如,在图形用户界面环境下,用户期望每个操作都能得到及时的反馈,轮转调度算法可以保证各个进程轮流执行,使得用户操作不会被长时间阻塞。

而在实时系统中,满足截止时间是最重要的目标。实时系统通常采用抢占式调度算法,如实时优先级调度,确保紧急任务能够在规定的时间内得到处理。例如,在飞机飞行控制系统中,对响应时间的要求极为严格,任何延迟都可能导致严重后果,实时优先级调度算法可以确保关键任务优先执行。

4.2调度器的设计与实现

调度器的设计通常采用模块化的方式,以便于适应不同的系统需求和优化目标。以 Linux 的 CFS(完全公平调度器)为例,它通过为每个进程安排一个虚拟运行时钟 vruntime,实现了公平性。当一个进程得以执行时,vruntime 的值不断增大,而没有运行的进程的 vruntime 保持不变。调度器总是选择 vruntime 最小的进程执行,从而确保每个进程都能获得相对公平的 CPU 时间。

CFS 的设计思路简单而有效,它根据每个进程的权重分配运行时间。例如,假设有两个进程 A 和 B,权重分别为 1 和 2,调度周期为 30ms。那么进程 A 的运行时间是 30*(1)/(1+2)=10ms,进程 B 的运行时间是 30*(2)/(1+2)=20ms。同时,CFS 通过调整 vruntime 的增长速度来体现不同进程的优先级,优先级高的进程 vruntime 增长得较慢,从而获得更多的运行机会。

为了降低调度延迟带来的不公平性,CFS 采用了红黑树数据结构来管理就绪队列。红黑树可以快速地找到 vruntime 最小的进程,从而减少调度时间。此外,CFS 还支持按组来分配时间片,通过 cgroup 机制,可以将 CPU 资源划分为不同的组,以便更好地满足不同应用场景的需求。

4.3进程状态的转换机制

进程在其生命周期内会经历多种状态的转换,这些转换受到资源分配的影响。例如,当一个进程从创建态转变为就绪态时,它需要获得除 CPU 以外的所有必要资源。一旦这些资源分配完成,进程就处于就绪状态,等待 CPU 的分配。

当进程获得 CPU 资源并开始执行时,它处于运行态。然而,在运行过程中,进程可能会因为等待某个事件(如等待 I/O 操作完成、等待资源分配等)而进入等待态。在等待态下,进程会被暂时挂起,以释放 CPU 资源供其他进程使用。当等待的事件发生时,进程会被唤醒并重新进入就绪态。

此外,进程还可能因为时间片用完或被更高优先级的进程抢占 CPU 而从运行态回到就绪态。而当一个进程完成其任务或出现无法克服的错误时,它会进入终止态,等待操作系统进行善后处理。

五、进程调度在实际中的应用

5.1在数据库中的应用

在 MySQL 中,进程调度起着至关重要的作用。进程调度的高效与否直接影响着数据库的响应时间和系统利用率。例如,在 MySQL 的 innodb 存储引擎中,通过合理的进程调度,可以有效地提高数据的读写速度,减少查询响应时间。据统计,在一些高并发的数据库应用场景中,优化进程调度算法可以使数据库的响应时间降低 20% 至 30%。

同时,MySQL 中的进程调度还涉及到锁机制和资源竞争的处理。例如,当多个事务同时访问同一数据行时,进程调度需要合理地安排事务的执行顺序,以避免死锁和提高系统的并发性能。通过合理的调度策略,可以有效地减少锁等待时间,提高数据库的吞吐量。研究表明,在特定的工作负载下,优化后的进程调度可以使 MySQL 的吞吐量提高 15% 至 20%。

⑴案例分析

假设我们有一个简单的关系型数据库系统,其中包含多个客户端连接,每个客户端都可能发起查询、插入、更新或删除等操作。数据库服务器需要有效地管理这些操作,以确保系统的响应时间和吞吐量。

  • 查询操作:当一个客户端发起查询请求时,数据库服务器需要分配一个进程来处理这个请求。这个进程需要从磁盘中读取数据,进行查询处理,并将结果返回给客户端。如果同时有多个查询请求,数据库服务器需要根据进程调度算法来决定哪个查询请求先被处理。

  • 插入、更新和删除操作:这些操作也需要分配进程来处理。与查询操作不同的是,这些操作可能需要修改数据库中的数据,因此需要考虑数据的一致性和锁机制。进程调度算法需要确保这些操作能够在不影响其他操作的情况下高效地执行。

  • 后台任务:数据库系统通常还会有一些后台任务,如日志记录、备份和恢复等。这些任务也需要分配进程来执行,并且需要与前台的客户端操作进行协调,以避免影响系统的性能。

⑵代码实现示例(伪代码)

以下是一个简单的数据库系统中进程调度的伪代码示例:

# 数据库操作队列
operation_queue = []

# 进程调度函数
def schedule_processes():
    while True:
        if operation_queue:
            operation = operation_queue.pop(0)
            # 根据操作类型分配进程
            if operation.type == "query":
                process_query(operation)
            elif operation.type in ["insert", "update", "delete"]:
                process_data_operation(operation)
            elif operation.type == "background_task":
                process_background_task(operation)
        else:
            # 如果没有操作,则等待
            wait_for_operation()

# 查询操作处理函数
def process_query(query_operation):
    # 分配进程处理查询请求
    # 从磁盘中读取数据,进行查询处理,并将结果返回给客户端
    data = read_data_from_disk(query_operation.table_name, query_operation.query_condition)
    result = process_query_data(data, query_operation.query_expression)
    return_result_to_client(query_operation.client_id, result)

# 数据操作处理函数
def process_data_operation(data_operation):
    # 分配进程处理插入、更新或删除操作
    # 获取锁,修改数据,释放锁
    acquire_lock(data_operation.table_name)
    if data_operation.type == "insert":
        insert_data(data_operation.table_name, data_operation.data)
    elif data_operation.type == "update":
        update_data(data_operation.table_name, data_operation.data, data_operation.update_condition)
    elif data_operation.type == "delete":
        delete_data(data_operation.table_name, data_operation.delete_condition)
    release_lock(data_operation.table_name)

# 后台任务处理函数
def process_background_task(background_task):
    # 分配进程处理后台任务
    if background_task.type == "log":
        log_data(background_task.data)
    elif background_task.type == "backup":
        backup_data()
    elif background_task.type == "restore":
        restore_data(background_task.data)

# 客户端发起操作函数
def client_operation(client_id, operation_type, table_name, data=None, query_condition=None, update_condition=None, delete_condition=None):
    operation = {
        "client_id": client_id,
        "type": operation_type,
        "table_name": table_name,
        "data": data,
        "query_condition": query_condition,
        "update_condition": update_condition,
        "delete_condition": delete_condition
    }
    operation_queue.append(operation)

# 主函数
def main():
    # 启动进程调度
    schedule_processes()

if __name__ == "__main__":
    main()

在这个示例中,我们使用一个操作队列来存储客户端发起的操作和后台任务。进程调度函数不断地从队列中取出操作,并根据操作类型分配进程来处理。查询操作从磁盘中读取数据并进行查询处理,数据操作需要获取锁来确保数据的一致性,后台任务则根据任务类型进行相应的处理。

5.2在游戏中的应用

在游戏开发中,进程创建和调度对于提升系统整体效率和稳定性至关重要。以热门游戏《堡垒之夜》为例,游戏中的角色创建过程可以类比为 Linux 系统中进程的创建。当玩家决定创建一个新角色时,游戏引擎会复制当前角色的属性,就如同 Linux 中的 fork 系统调用复制当前进程的内容生成一个新的子进程。

而游戏中的回合制机制则可以类比为进程调度中的轮转调度算法。在回合制游戏中,每个玩家轮流进行操作,这与轮转调度中每个进程轮流获得固定时间片的 CPU 使用权类似。通过这种方式,可以确保每个玩家都能在合理的时间内进行操作,提高游戏的公平性和流畅度。

此外,游戏中的进程调度还需要考虑不同类型任务的优先级。例如,在《英雄联盟》中,实时的战斗场景需要高优先级的处理,以确保玩家的操作能够及时响应。而一些后台任务,如资源加载和更新,则可以分配较低的优先级。通过合理的优先级调度,可以在不影响游戏性能的前提下,提高系统的整体效率。

⑴案例分析

假设我们正在开发一款角色扮演游戏(RPG),游戏中有多个角色、怪物、环境特效等元素。为了实现游戏的流畅运行,我们需要合理地调度这些元素的更新和渲染过程。

  • 角色行为更新:每个角色都有自己的行为逻辑,如移动、攻击、施法等。这些行为需要定期更新,以确保角色能够根据游戏状态做出相应的反应。例如,一个怪物可能会追逐玩家角色,而玩家角色则需要根据怪物的位置和自身的属性来决定下一步的行动。

  • 怪物 AI 更新:怪物通常具有一定的人工智能(AI),它们需要根据游戏环境和玩家的行为来做出决策。例如,怪物可能会巡逻、攻击玩家、逃跑等。这些 AI 逻辑也需要定期更新,以确保怪物的行为具有一定的智能性。

  • 环境特效更新:游戏中的环境特效,如火焰、烟雾、水流等,也需要定期更新,以增强游戏的视觉效果。例如,火焰特效可能会随着时间的推移而变化,烟雾特效可能会根据风向和风力而扩散。

  • 渲染过程:游戏的渲染过程需要将游戏世界中的各个元素绘制到屏幕上。这个过程需要消耗大量的计算资源,因此需要合理地调度,以确保游戏能够在不同的硬件平台上流畅运行。例如,我们可以根据游戏的帧率和硬件性能来调整渲染的细节级别,以提高游戏的性能。

⑵代码实现示例(使用 C++ 和游戏引擎框架,如 Unreal Engine 或 Unity)

以下是一个简单的游戏进程调度示例代码,使用 C++ 和虚幻引擎(Unreal Engine)框架:

// 游戏角色类
class AGameCharacter
{
public:
    void Update()
    {
        // 更新角色的行为逻辑
        // 例如,移动、攻击、施法等
    }
};

// 怪物类
class AGameMonster : public AGameCharacter
{
public:
    void UpdateAI()
    {
        // 更新怪物的 AI 逻辑
        // 例如,巡逻、攻击玩家、逃跑等
    }
};

// 环境特效类
class AGameEnvironmentEffect
{
public:
    void UpdateEffect()
    {
        // 更新环境特效
        // 例如,火焰、烟雾、水流等
    }
};

// 游戏进程调度类
class GameProcessScheduler
{
public:
    void UpdateGame()
    {
        // 更新角色
        for (AGameCharacter* character : characters)
        {
            character->Update();
        }

        // 更新怪物的 AI
        for (AGameMonster* monster : monsters)
        {
            monster->UpdateAI();
        }

        // 更新环境特效
        for (AGameEnvironmentEffect* effect : environmentEffects)
        {
            effect->UpdateEffect();
        }

        // 进行渲染
        Render();
    }

private:
    TArray<AGameCharacter*> characters;
    TArray<AGameMonster*> monsters;
    TArray<AGameEnvironmentEffect*> environmentEffects;

    void Render()
    {
        // 进行游戏渲染
        // 根据帧率和硬件性能调整渲染细节级别
    }
};

在这个示例中,我们定义了游戏角色类、怪物类和环境特效类,它们都有自己的更新方法。游戏进程调度类负责定期调用这些更新方法,并进行游戏的渲染。在实际的游戏开发中,我们可以根据游戏的具体需求和架构,进一步扩展和优化这个进程调度机制。

这只是一个简单的示例,实际的游戏开发中的进程调度要复杂得多。在实际应用中,我们还需要考虑更多的因素,如多线程、资源管理、性能优化等。此外,不同的游戏引擎框架可能提供不同的进程调度机制和工具,我们可以根据具体的需求选择合适的方法来实现游戏的进程调度。

5.3在不同操作系统环境中的应用

在批处理环境中,主要目标是提高系统的吞吐量和减少平均周转时间。例如,在一些大型数据处理中心,采用短作业优先调度算法可以优先处理执行时间短的作业,从而在单位时间内完成更多的任务。据统计,在批处理系统中使用短作业优先算法可以使系统吞吐量提高 15% 至 20%。

在交互式环境中,响应时间是关键指标。此时,轮转调度算法或优先级调度算法可能更为合适。例如,在图形用户界面环境下,用户期望每个操作都能得到及时的反馈,轮转调度算法可以保证各个进程轮流执行,使得用户操作不会被长时间阻塞。而优先级调度算法可以根据任务的重要性和紧急程度分配不同的优先级,确保关键任务能够优先得到处理。

在实时环境中,满足截止时间是最重要的目标。实时系统通常采用抢占式调度算法,如实时优先级调度,确保紧急任务能够在规定的时间内得到处理。例如,在医疗设备控制系统中,对响应时间的要求极为严格,任何延迟都可能导致严重后果,实时优先级调度算法可以确保关键任务优先执行。

以下是进程调度在不同操作系统环境中的应用案例分析及简单的代码实现示例(这里以 Linux 和 Windows 为例,采用伪代码风格来说明概念)。

⑴Linux 中的进程调度案例分析

在 Linux 中,常用的进程调度算法有完全公平调度算法(CFS)等。

  • 案例:假设在一个服务器环境中,运行着多个不同优先级的服务进程。高优先级的进程可能是关键业务服务,需要尽快得到响应;低优先级的进程可能是一些后台任务,如数据备份等。

  • 调度特点:CFS 试图确保每个进程都能公平地获得 CPU 时间,同时根据进程的优先级进行适当调整。高优先级进程会获得更多的 CPU 时间份额。例如,当系统负载较高时,高优先级的服务进程会更频繁地被调度执行,以保证关键业务的响应时间。

Linux 环境下伪代码示例(模拟进程调度):

# 假设定义了进程类
class Process:
    def __init__(self, name, priority):
        self.name = name
        self.priority = priority

# 模拟进程队列
process_queue = [
    Process("HighPriorityProcess", 2),
    Process("LowPriorityProcess", 1)
]

def schedule_linux():
    while process_queue:
        # 根据优先级排序
        process_queue.sort(key=lambda p: p.priority, reverse=True)
        current_process = process_queue.pop(0)
        print(f"Scheduling {current_process.name}")
        # 模拟执行一段时间
        #...

⑵Windows 中的进程调度案例分析

在 Windows 中,采用基于优先级的抢占式调度。

  • 案例:在一个图形设计软件的使用场景中,用户交互进程(如响应鼠标和键盘事件的进程)需要及时响应,而一些长时间运行的渲染进程可以在后台慢慢执行。

  • 调度特点:Windows 根据进程的优先级类别(如实时、高、高于正常、正常、低于正常、低等)和线程的优先级级别来决定哪个进程或线程先获得 CPU 时间。用户交互进程通常被赋予较高的优先级,以确保良好的用户体验。

Windows 环境下伪代码示例(模拟进程调度):

class WindowsProcess:
    def __init__(self, name, priority_level):
        self.name = name
        self.priority_level = priority_level

# 模拟进程队列
windows_process_queue = [
    WindowsProcess("UserInteractionProcess", "High"),
    WindowsProcess("RenderingProcess", "Normal")
]

def schedule_windows():
    while windows_process_queue:
        # 根据优先级选择进程
        if "High" in [p.priority_level for p in windows_process_queue]:
            current_process = [p for p in windows_process_queue if p.priority_level == "High"][0]
        else:
            current_process = windows_process_queue[0]
        print(f"Scheduling {current_process.name}")
        # 模拟执行一段时间
        #...

网站公告

今日签到

点亮在社区的每一天
去签到