文章目录
1. 前言
The GNU C Library Reference Manual for version 2.35
2. 资源使用和限制
本章描述了用于检查进程使用了多少各种资源(CPU 时间、内存等)以及获取和设置未来使用限制的函数。
2.1. 资源使用
函数 getrusage 和数据类型 struct rusage 用于检查进程的资源使用情况。它们在 sys/resource.h 中声明。
函数:int getrusage (int processes, struct rusage *rusage)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数报告由进程指定的进程的资源使用总量,将信息存储在 *rusage 中。
在大多数系统中,进程只有两个有效值:
RUSAGE_SELF
只是目前的过程。
RUSAGE_CHILDREN
所有已经终止的子进程(直接和间接)。
getrusage 的返回值为零表示成功,-1 表示失败。
EINVAL
参数过程无效。
获取特定子进程资源使用情况的一种方法是使用函数 wait4,该函数在子进程终止时返回总计。请参阅 BSD 进程等待函数。
数据类型:struct rusage
此数据类型存储各种资源使用统计信息。它有以下成员,可能还有其他成员:
struct timeval ru_utime
执行用户指令所花费的时间。
struct timeval ru_stime
代表进程在操作系统代码中花费的时间。
long int ru_maxrss
使用的最大驻留集大小,以千字节为单位。即,进程同时使用的物理内存的最大千字节数。
long int ru_ixrss
以千字节表示的整数值乘以执行的滴答声,表示与其他进程共享的文本使用的内存量。
long int ru_idrss
以相同方式表示的整数值,即用于数据的非共享内存量。
long int ru_isrss
一个整数值以同样的方式表示,它是用于堆栈空间的非共享内存量。
long int ru_minflt
无需任何 I/O 即可处理的页面错误数。
long int ru_majflt
通过执行 I/O 服务的页面错误数。
long int ru_nswap
进程完全从主内存中换出的次数。
long int ru_inblock
文件系统必须代表进程从磁盘读取的次数。
long int ru_oublock
文件系统必须代表进程写入磁盘的次数。
long int ru_msgsnd
发送的 IPC 消息数。
long int ru_msgrcv
收到的 IPC 消息数。
long int ru_nsignals
接收到的信号数。
long int ru_nvcsw
进程自愿调用上下文切换的次数(通常是为了等待某些服务)。
long int ru_nivcsw
发生非自愿上下文切换的次数(因为时间片已过期,或安排了另一个更高优先级的进程)。
2.2. 限制资源使用
Limiting Resource Usage
您可以指定进程的资源使用限制。当进程试图超过限制时,它可能会收到一个信号,或者它尝试这样做的系统调用可能会失败,具体取决于资源。每个进程最初都从其父进程继承其限制值,但随后可以更改它们。
有两个与资源相关的每进程限制:
-
current limit
当前限制是系统不允许使用超过的值。也被称为“软限制”,因为被限制的进程一般可以随意提高电流限制。
-
maximum limit
最大限制是允许进程设置其当前限制的最大值。它也被称为“硬限制”,因为进程无法绕过它。进程可以降低自己的最大限制,但只有超级用户可以增加最大限制。
用于 getrlimit、setrlimit、getrlimit64 和 setrlimit64 的符号在 sys/resource.h 中定义。
函数:int getrlimit (int resource, struct rlimit *rlp)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
读取资源资源的当前和最大限制并将它们存储在 *rlp 中。
成功时返回值为 0,失败时返回值为 -1。唯一可能的 errno 错误条件是 EFAULT。
在 32 位系统上使用 _FILE_OFFSET_BITS == 64 编译源代码时,此函数实际上是 getrlimit64。因此,LFS 接口透明地替换了旧接口。
函数:int getrlimit64 (int resource, struct rlimit64 *rlp)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数类似于 getrlimit,但它的第二个参数是指向 struct rlimit64 类型变量的指针,它允许它读取不适合 struct rlimit 成员的值。
如果在 32 位机器上使用 _FILE_OFFSET_BITS == 64 编译源代码,则此函数在名称 getrlimit 下可用,因此可以透明地替换旧接口。
函数:int setrlimit (int resource, const struct rlimit *rlp)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
将资源资源的当前和最大限制存储在 *rlp 中。
成功时返回值为 0,失败时返回值为 -1。可能出现以下 errno 错误情况:
EPERM
- 该过程试图将电流限制提高到超过最大限制。
- 该进程试图提高最大限制,但不是超级用户。
在 32 位系统上使用 _FILE_OFFSET_BITS == 64 编译源代码时,此函数实际上是 setrlimit64。因此,LFS 接口透明地替换了旧接口。
函数:int setrlimit64 (int resource, const struct rlimit64 *rlp)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数类似于 setrlimit,但它的第二个参数是指向 struct rlimit64 类型变量的指针,它允许它设置不适合 struct rlimit 成员的值。
如果在 32 位机器上使用 _FILE_OFFSET_BITS == 64 编译源代码,则此函数在名称 setrlimit 下可用,因此可以透明地替换旧接口。
数据类型:struct rlimit
此结构与 getrlimit 一起使用以接收限制值,并与 setrlimit 一起使用以指定特定进程和资源的限制值。它有两个字段:
rlim_t rlim_cur
电流限制
rlim_t rlim_max
最大限制。
对于getrlimit,结构是一个输出;它接收当前值。对于 setrlimit,它指定新值。
对于 LFS 函数,在 sys/resource.h 中定义了类似的类型。
数据类型:struct rlimit64
这个结构类似于上面的 rlimit 结构,但它的组件范围更广。它有两个字段:
rlim64_t rlim_cur
这类似于 rlimit.rlim_cur,但类型不同。
rlim64_t rlim_max
这类似于 rlimit.rlim_max,但类型不同。
以下是您可以指定限制的资源列表。内存和文件大小以字节为单位。
RLIMIT_CPU
进程可以使用的最大 CPU 时间。如果它运行的时间超过这个时间,它会收到一个信号:SIGXCPU。该值以秒为单位。请参阅操作错误信号。
RLIMIT_FSIZE
进程可以创建的文件的最大大小。尝试写入更大的文件会导致一个信号:SIGXFSZ。请参阅操作错误信号。
RLIMIT_DATA
进程的最大数据内存大小。如果进程试图分配超出此数量的数据内存,则分配函数将失败。
RLIMIT_STACK
进程的最大堆栈大小。如果进程试图将它的堆栈扩展超过这个大小,它会收到一个 SIGSEGV 信号。请参阅程序错误信号。
RLIMIT_CORE
此进程可以创建的最大核心文件大小。如果进程终止并转储比此更大的核心文件,则不会创建核心文件。因此,将此限制设置为零可防止创建核心文件。
RLIMIT_RSS
此进程应获得的最大物理内存量。该参数是系统调度器和内存分配器的指南;当有剩余时,系统可能会给进程更多的内存。
RLIMIT_MEMLOCK
可以锁定到物理内存中的最大内存量(因此永远不会被调出)。
RLIMIT_NPROC
同一用户 ID 可以创建的最大进程数。如果您已达到用户 ID 的限制,则 fork 将失败并显示 EAGAIN。请参阅创建进程。
RLIMIT_NOFILE
RLIMIT_OFILE
进程可以打开的最大文件数。如果它试图打开比这更多的文件,它的打开尝试会失败并显示 errno EMFILE。请参阅错误码。并非所有系统都支持此限制;GNU 可以,4.4 BSD 可以。
RLIMIT_AS
此进程应获得的总内存的最大大小。如果进程尝试使用 brk、malloc、mmap 或 sbrk 分配超出此数量的更多内存,则分配函数将失败。
RLIM_NLIMITS
不同资源限制的数量。任何有效的资源操作数都必须小于 RLIM_NLIMITS。
常数:rlim_t RLIM_INFINITY
当作为 setrlimit 中的限制值提供时,此常量代表“无穷大”值。
以下是执行上述功能的一些历史功能。上面的函数是更好的选择。
ulimit 和命令符号在 ulimit.h 中声明。
函数:long int ulimit (int cmd, ...)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
ulimit 根据命令 cmd 获取当前限制或为调用进程设置特定资源的当前和最大限制。
如果你得到一个限制,命令参数是唯一的参数。如果要设置限制,还有第二个参数:long int limit,它是您要设置限制的值。
cmd 值和它们指定的操作是:
GETFSIZE
获取文件大小的当前限制,以 512 字节为单位。
SETFSIZE
将文件大小的当前和最大限制设置为限制 * 512 字节。
还有一些其他的 cmd 值可能会在某些系统上执行某些操作,但它们不受支持。
只有超级用户可以增加最大限制。
当你成功获得一个限制时,ulimit 的返回值就是那个限制,它永远不会是负数。成功设置限制后,返回值为零。函数失败时,返回值为-1,根据原因设置errno:
EPERM
一个进程试图增加最大限制,但不是超级用户。
vlimit 及其资源符号在 sys/vlimit.h 中声明。
函数:int vlimit (int resource, int limit)
Preliminary: | MT-Unsafe race:setrlimit | AS-Unsafe | AC-Safe | See POSIX Safety Concepts.
vlimit 为进程设置资源的当前限制。
resource 标识资源:
LIM_CPU
最大 CPU 时间。与 setrlimit 的 RLIMIT_CPU 相同。
LIM_FSIZE
最大文件大小。与 setrlimit 的 RLIMIT_FSIZE 相同。
LIM_DATA
最大数据存储器。与 setrlimit 的 RLIMIT_DATA 相同。
LIM_STACK
最大堆栈大小。与 setrlimit 的 RLIMIT_STACK 相同。
LIM_CORE
最大核心文件大小。与 setrlimit 的 RLIMIT_COR 相同。
LIM_MAXRSS
最大物理内存。与 setrlimit 的 RLIMIT_RSS 相同。
返回值为 0 表示成功,-1 并相应地设置 errno 表示失败:
EPERM
该进程试图将其当前限制设置为超出其最大限制。
2.3. 进程CPU优先级和调度
Process CPU Priority And Scheduling
当多个进程同时需要 CPU 时间时,系统的调度策略和进程 CPU 优先级决定了哪些进程得到它。本节描述了如何做出该决定以及控制它的 GNU C 库函数。
通常将 CPU 调度简称为调度,将进程的 CPU 优先级简称为进程的优先级,其中隐含了 CPU 资源。但请记住,CPU 时间并不是进程使用或进程竞争的唯一资源。在某些情况下,它甚至不是特别重要。给一个进程一个高“优先级”可能对一个进程相对于其他进程的运行速度影响很小。本节讨论的优先级仅适用于 CPU 时间。
CPU 调度是一个复杂的问题,不同的系统以完全不同的方式进行处理。新的想法不断发展,并在各种系统的调度算法的复杂性中找到了自己的方式。本节讨论一般概念、通常使用 GNU C 库的系统的一些细节以及一些标准。
为简单起见,我们将 CPU 争用视为系统中只有一个 CPU。但是,当一个处理器有多个 CPU 时,所有相同的原则都适用,并且知道在任何时候可以运行的进程数等于 CPU 的数量,您可以轻松推断出这些信息。
本节中描述的函数均由 POSIX.1 和 POSIX.1b 标准定义(sched… 函数为 POSIX.1b)。但是,POSIX 没有为这些函数获取和设置的值定义任何语义。在本章中,语义基于 Linux 内核对 POSIX 标准的实现。正如您将看到的,Linux 实现与 POSIX 语法的作者所想的完全相反。
2.3.1. 绝对优先
Absolute Priority
每个进程都有一个绝对的优先级,并用一个数字表示。数字越大,绝对优先级越高。
在过去的系统和今天的大多数系统上,所有进程的绝对优先级都为 0,这部分无关紧要。在这种情况下,请参阅传统调度。发明了绝对优先级以适应实时系统,其中某些进程能够实时响应发生的外部事件至关重要,这意味着它们不能等待而其他一些想要但不需要运行的进程占用CPU。
当两个进程在任何时刻争用 CPU 时,具有较高绝对优先级的总是得到它。即使具有较低优先级的进程已经在使用 CPU(即调度是抢占式的)也是如此。当然,我们只讨论正在运行或“准备运行”的进程,这意味着它们现在已准备好执行指令。当一个进程阻塞等待 I/O 之类的东西时,它的绝对优先级是无关紧要的。
注意:术语“runnable”是“ready to run”的同义词。
当两个进程正在运行或准备运行并且都具有相同的绝对优先级时,它会更有趣。在这种情况下,谁获得 CPU 由调度策略决定。如果进程的绝对优先级为 0,则应用传统调度中描述的传统调度策略。否则,应用实时调度中描述的策略。
您通常只将高于 0 的绝对优先级赋予可以信任不会占用 CPU 的进程。此类进程旨在在相对较短的 CPU 运行后阻塞(或终止)。
进程以与其父进程相同的绝对优先级开始生命。基本调度功能中描述的功能可以改变它。
只有特权进程才能将进程的绝对优先级更改为 0 以外的值。只有特权进程或目标进程的所有者才能完全更改其绝对优先级。
POSIX 要求与实时调度策略一起使用的绝对优先级值在至少 32 的范围内是连续的。在 Linux 上,它们是 1 到 99。函数 sched_get_priority_max 和 sched_set_priority_min 可移植地告诉您特定系统上的范围是多少。
2.3.1.1. 使用绝对优先级
Using Absolute Priority
在设计实时应用程序时必须记住的一件事是,具有比任何其他进程更高的绝对优先级并不能保证进程可以连续运行。可能破坏良好 CPU 运行的两件事是中断和页面错误。
中断处理程序处于进程之间的边缘。CPU 正在执行指令,但它们不是任何进程的一部分。即使是最高优先级的进程,中断也会停止。因此,您必须允许轻微的延迟,并确保系统中的任何设备都没有中断处理程序,这可能会导致您的进程指令之间的延迟过长。
类似地,页面错误会导致看似简单的指令序列需要很长时间。其他进程在页面错误进入时运行这一事实并不重要,因为一旦 I/O 完成,更高优先级的进程会将它们踢出并再次运行,但等待 I/O 本身可能是个问题。要消除这种威胁,请使用 mlock 或 mlockall。
当您选择设置优先级以及处理以高绝对优先级运行的程序时,您需要牢记此优先级在单 CPU 系统上的绝对性的一些后果。考虑一个比系统中任何其他进程具有更高绝对优先级的进程,并且由于其程序中的错误,它进入了无限循环。它永远不会放弃 CPU。您不能运行命令来杀死它,因为您的命令需要获取 CPU 才能运行。错误的程序完全处于控制之中。它控制垂直,它控制水平。
有两种方法可以避免这种情况:1) 让 shell 在具有更高绝对优先级的地方运行或 2) 让控制终端连接到高优先级进程组。如果您按下 Control-C,世界上所有的优先级都不会阻止中断处理程序运行并向进程传递信号。
一些系统使用绝对优先级作为将固定百分比的 CPU 时间分配给进程的方法。为此,一个超高优先级特权进程会持续监控进程的 CPU 使用率,并在进程未获得其授权份额时提高其绝对优先级,并在进程超过它时降低其绝对优先级。
注意:绝对优先级有时也称为“静态优先级”。我们在本手册中不使用该术语,因为它忽略了绝对优先级的最重要特征:它的绝对性。
2.3.2. 实时调度
Realtime Scheduling
每当两个具有相同绝对优先级的进程准备好运行时,内核就会做出决定,因为一次只能运行一个。如果进程的绝对优先级为 0,内核会按照传统调度中的说明做出此决定。否则,决定如本节所述。
如果两个进程已准备好运行但具有不同的绝对优先级,则决策要简单得多,并在绝对优先级中进行了描述。
每个进程都有一个调度策略。对于绝对优先级不为零的进程,有两种可用:
- First Come First Served 先到先得
- Round Robin 循环赛
最明智的情况是所有具有一定绝对优先级的进程都具有相同的调度策略。我们将首先讨论。
在 Round Robin 中,进程共享 CPU,每个进程运行一小段时间(“时间片”),然后以循环方式让步给另一个进程。当然,只有准备好运行并且具有相同绝对优先级的进程才在这个循环中。
在“先到先服务”中,等待运行时间最长的进程获得 CPU,并一直保留它,直到它自愿放弃 CPU、用完待办事项(阻塞)或被更高优先级的进程抢占。
先到先服务,以及最大的绝对优先级和对中断和页面错误的仔细控制,是当进程绝对、肯定必须以全 CPU 速度运行或根本不运行时使用的方法。
具有先到先服务调度策略的进程明智地使用 sched_yield 函数调用形成了循环和先到先服务之间的良好折衷。
要了解当不同调度策略的进程占用相同的绝对优先级时调度是如何工作的,您必须了解进程如何进入和退出准备运行列表的细节。
在这两种情况下,准备运行列表都组织为一个真正的队列,其中一个进程在准备好运行时被推到尾部,而当调度程序决定运行它时,它从头部弹出。注意准备运行和运行是两个互斥的状态。当调度程序运行一个进程时,该进程不再准备好运行并且不再在准备运行列表中。当进程停止运行时,它可能会重新准备好再次运行。
分配了循环调度策略的进程和分配了先到先服务的进程之间的唯一区别在于,在前一种情况下,进程会在一定时间后自动从 CPU 引导。发生这种情况时,该进程将返回准备运行,这意味着它在尾部进入队列。我们所说的时间量子很小。真的很小。这不是你父亲的分时制。例如,对于 Linux 内核,循环时间片比传统调度的典型时间片短一千倍。
进程开始时使用与其父进程相同的调度策略。基本调度功能中描述的功能可以改变它。
只有特权进程才能设置绝对优先级高于 0 的进程的调度策略。
2.3.3. 基本调度函数
Basic Scheduling Functions
本节介绍 GNU C 库中用于设置进程的绝对优先级和调度策略的函数。
可移植性注意:在具有本节功能的系统上,宏 _POSIX_PRIORITY_SCHEDULING 在 <unistd.h> 中定义。
对于调度策略为传统调度的情况,更多微调调度的功能在传统调度中。
不要试图过多地利用这些函数的命名和结构。它们与本手册中描述的概念不匹配,因为这些函数是由 POSIX.1b 定义的,但是在使用 GNU C 库的系统上的实现与 POSIX 结构所设想的相反。POSIX 方案假定主要调度参数是调度策略,优先级值(如果有)是调度策略的参数。但是,在实现中,优先级值是王道,调度策略(如果有的话)只会微调该优先级的效果。
本节中的符号是通过包含文件 sched.h 来声明的。
可移植性注意:在 POSIX 中,以下函数的 pid_t 参数指的是进程 ID。在 Linux 上,它们实际上是线程 ID,并控制如何针对整个系统调度特定线程。产生的行为不符合 POSIX。这就是为什么以下描述指的是任务和任务 ID,而不是进程和进程 ID。
数据类型:struct sched_param
该结构描述了绝对优先级。
int sched_priority
绝对优先值
函数:int sched_setscheduler (pid_t pid, int policy, const struct sched_param *param)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
该函数设置任务的绝对优先级和调度策略。
它将 param 给出的绝对优先级值和调度策略策略分配给 ID 为 pid 的任务,或者如果 pid 为零则调用任务。如果 policy 为负,sched_setscheduler 会保留现有的调度策略。
以下宏表示策略的有效值:
SCHED_OTHER
传统调度
SCHED_FIFO
先进先出
SCHED_RR
循环赛
成功时,返回值为 0。否则,为 -1,并相应地设置 ERRNO。此函数特定的 errno 值是:
EPERM
调用任务没有 CAP_SYS_NICE 权限并且策略不是 SCHED_OTHER(或者它是否定的并且现有策略不是 SCHED_OTHER。
调用任务没有 CAP_SYS_NICE 权限,并且它的所有者不是目标任务的所有者。即调用任务的有效 uid 既不是任务 pid 的有效 uid 也不是真正的 uid。
ESRCH
pid 没有任务 pid 并且 pid 不为零。
EINVAL
policy 不标识现有的调度策略。
*param 标识的绝对优先级值超出调度策略策略的有效范围(或如果策略为负,则为现有调度策略)或 param 为空。sched_get_priority_max 和 sched_get_priority_min 告诉你有效范围是多少。
pid 为负数。
函数:int sched_getscheduler (pid_t pid)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回分配给 ID 为 pid 的任务的调度策略,如果 pid 为零,则返回调用任务。
返回值是调度策略。有关可能的值,请参见 sched_setscheduler。
如果函数失败,则返回值为 -1 并相应地设置 errno。
此函数特定的 errno 值是:
ESRCH
pid 没有任务 pid 不为零。
EINVAL
pid 为负数。
请注意,此函数与 sched_setscheduler 不完全匹配,因为虽然该函数设置调度策略和绝对优先级,但此函数仅获取调度策略。要获得绝对优先级,请使用 sched_getparam。
函数:int sched_setparam (pid_t pid, const struct sched_param *param)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数设置任务的绝对优先级。
它在功能上与策略 = -1 的 sched_setscheduler 相同。
函数:int sched_getparam (pid_t pid, struct sched_param *param)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回任务的绝对优先级。
pid 是您想知道其绝对优先级的任务的任务 ID。
param 是指向函数存储任务绝对优先级的结构的指针。
成功时,返回值为 0。否则,为 -1 并相应地设置 errno。此函数特定的 errno 值是:
ESRCH
没有 ID 为 pid 的任务,它不为零。
EINVAL
pid 为负数。
函数:int sched_get_priority_min (int policy)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回具有调度策略策略的任务允许的最低绝对优先级值。
在 Linux 上,SCHED_OTHER 为 0,其他一切为 1。
成功时,返回值为 0。否则,为 -1,并相应地设置 ERRNO。此函数特定的 errno 值是:
EINVAL
policy 不标识现有的调度策略。
函数:int sched_get_priority_max (int policy)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回具有调度策略策略的任务允许的最高绝对优先级值。
在 Linux 上,SCHED_OTHER 为 0,其他一切为 99。
成功时,返回值为 0。否则,为 -1,并相应地设置 ERRNO。此函数特定的 errno 值是:
EINVAL
policy 不标识现有的调度策略。
函数:int sched_rr_get_interval (pid_t pid, struct timespec *interval)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回与循环调度策略一起使用的时间片(时间片)的长度(如果使用),用于任务 ID 为 pid 的任务。
它返回时间长度作为间隔。
对于 Linux 内核,循环时间片始终为 150 微秒,并且 pid 甚至不必是真正的 pid。
成功时返回值为 0,如果失败,则返回值为 -1 并相应地设置 errno。这个函数没有什么特别的问题,所以没有特定的 errno 值。
函数:int sched_yield (void)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
该函数自愿放弃任务对 CPU 的要求。
从技术上讲,sched_yield 导致调用任务立即准备好运行(与之前的运行相反)。这意味着如果它的绝对优先级高于 0,它将被推到共享其绝对优先级并准备运行的任务队列的尾部,并在下一次轮到它时再次运行。如果它的绝对优先级为 0,则更复杂,但仍然具有让 CPU 给其他任务的效果。
如果没有其他任务共享调用任务的绝对优先级,则此函数不起作用。
如果包含程序不知道系统中的其他进程正在做什么以及它执行的速度有多快,那么这个函数就表现为一个无操作。
成功时返回值为 0,如果失败,则返回值为 -1 并相应地设置 errno。这个函数没有什么特别的问题,所以没有特定的 errno 值。
2.3.4. 传统调度
Traditional Scheduling
本节讲的是绝对优先级为0的进程之间的调度。当系统将绝对优先级高的进程占尽所有剩余的CPU时间分配出去时,这里描述的调度决定了谁是最重要的。未清洗的过程得到它们。
2.3.4.1. 传统调度简介
Introduction To Traditional Scheduling
早在有绝对优先级(参见绝对优先级)之前,Unix 系统就使用这个系统来调度 CPU。当 POSIX 像罗马人一样出现并强加绝对优先级以适应实时处理的需求时,它让本地的绝对优先级零进程通过自己熟悉的调度策略来管理自己。
事实上,高于零的绝对优先级在今天的许多系统上都不可用,并且通常不会在它们可用时使用,主要用于进行实时处理的计算机。所以本节描述了许多程序员需要关注的唯一调度。
但要明确这个调度的范围:任何时候一个绝对优先级为 0 的进程和一个绝对优先级高于 0 的进程准备好同时运行,绝对优先级为 0 的进程不会运行.如果在更高优先级的准备运行进程出现时它已经在运行,它会立即停止。
除了绝对优先级为零之外,每个进程还有另一个优先级,我们将其称为“动态优先级”,因为它会随着时间而变化。动态优先级对于绝对优先级高于零的进程没有意义。
动态优先级有时决定谁获得下一次 CPU 启动权。有时它决定了转弯持续多长时间。有时它决定一个进程是否可以将另一个进程踢出 CPU。
在 Linux 中,值是这些东西的组合,但大多数情况下它只是决定时间片的长度。进程的动态优先级越高,它在 CPU 上获得的时间就越长。如果它在放弃 CPU 去做诸如等待 I/O 之类的事情之前没有用完它的时间片,那么它有利于在它准备好时让 CPU 回来,以完成它的时间片。除此之外,新时间片的进程选择基本上是循环的。但是调度器确实向低优先级进程扔了一块骨头:一个进程的动态优先级每次在调度过程中被冷落时都会上升。在 Linux 中,即使是胖子也能玩。
进程动态优先级的波动由另一个值调节:“nice”值。nice 值是一个整数,通常在 -20 到 20 的范围内,表示进程动态优先级的上限。nice 数越高,限制越低。
例如,在典型的 Linux 系统上,nice 值为 20 的进程一次只能在 CPU 上获得 10 毫秒,而 nice 值为 -20 的进程可以获得足够高的优先级来获得 400 毫秒。
nice 价值的概念是恭敬的礼貌。起初,在伊甸园的 Unix 园中,所有进程平等地共享计算机系统的资源。但并非所有进程都需要相同的 CPU 时间份额,因此 nice 值使有礼貌的进程能够拒绝其他可能繁荣的 CPU 时间份额。因此,进程的 nice 值越高,进程就越好。(然后一条蛇出现并为某些进程提供了一个负的 nice 值,这个系统就变成了我们今天所知道的粗鲁的资源分配系统。)
动态优先级倾向于向上和向下,目的是平滑 CPU 时间分配并为不频繁的请求提供快速响应时间。但是它们永远不会超过它们的 nice 限制,因此在负载较重的 CPU 上,nice 值有效地决定了进程运行的速度。
为了与 Unix 进程优先级的社会主义传统保持一致,一个进程以与其父进程相同的良好价值开始生命,并且可以随意提升它。一个进程还可以提高同一用户(或有效用户)拥有的任何其他进程的 nice 值。但是只有特权进程才能降低它的nice值。一个特权进程也可以提高或降低另一个进程的 nice 值。
用于获取和设置好的值的 GNU C 库函数在传统调度功能中进行了描述。
2.3.4.2. 传统调度的函数
Functions For Traditional Scheduling
本节介绍如何读取和设置进程的 nice 值。所有这些符号都在 sys/resource.h 中声明。
函数和宏名称由 POSIX 定义,并且指的是“优先级”,但函数实际上与不错的值有关,因为这些术语在手册和 POSIX 中都使用。
有效 nice 值的范围取决于内核,但通常从 -20 到 20 运行。nice 值越低,进程的优先级越高。这些常量描述了优先级值的范围:
PRIO_MIN
最低的有效 nice 值。
PRIO_MAX
最高的有效 nice 值。
函数:int getpriority (int class, int id)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
返回一组进程的nice值;class 和 id 指定哪些(见下文)。如果指定的进程并非都具有相同的 nice 值,则返回其中任何进程具有的最低值。
成功时,返回值为 0。否则,为 -1 并相应地设置 errno。此函数特定的 errno 值是:
ESRCH
class 和 id 的组合与任何现有进程都不匹配。
EINVAL
类的值无效。
如果返回值为 -1,则可能表示失败,也可能是 nice 值。唯一确定的方法是在调用 getpriority 之前设置 errno = 0,然后使用 errno != 0 作为失败的标准。
函数:int setpriority (int class, int id, int niceval)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
将一组进程的nice值设置为niceval;class 和 id 指定哪些(见下文)。
返回值为 0 表示成功,-1 表示失败。此函数可能出现以下 errno 错误情况:
ESRCH
class 和 id 的组合与任何现有进程都不匹配。
EINVAL
类的值无效。
EPERM
该调用将设置由与调用进程不同的用户拥有的进程的 nice 值(即,目标进程的真实或有效 uid 与调用进程的有效 uid 不匹配)并且调用进程没有 CAP_SYS_NICE允许。
EACCES
该调用会降低进程的 nice 值,并且该进程没有 CAP_SYS_NICE 权限。
参数 class 和 id 一起指定了一组您感兴趣的进程。这些是类的可能值:
PRIO_PROCESS
一个特定的过程。参数 id 是进程 ID (pid)。
PRIO_PGRP
特定进程组中的所有进程。参数 id 是进程组 ID (pgid)。
PRIO_USER
特定用户拥有的所有进程(即,其真实 uid 表示该用户)。参数 id 是用户 ID (uid)。
如果参数 id 是 0,它代表调用进程,它的进程组,或者它的所有者(真实的 uid),根据类。
函数:int nice (int increment)
Preliminary: | MT-Unsafe race:setpriority | AS-Unsafe | AC-Safe | See POSIX Safety Concepts.
按增量递增调用进程的 nice 值。返回值是成功时的新 nice 值,失败时为 -1。在失败的情况下,errno 将被设置为与 setpriority 相同的值。
这是 nice 的等效定义:
int
nice (int increment)
{
int result, old = getpriority (PRIO_PROCESS, 0);
result = setpriority (PRIO_PROCESS, 0, old + increment);
if (result != -1)
return old + increment;
else
return -1;
}
2.3.5. 限制对某些 CPU 的执行
Limiting execution to certain CPUs
在多处理器系统上,操作系统通常以允许系统最有效地工作的方式分配可在所有可用 CPU 上运行的不同进程。哪些进程和线程运行在某种程度上可以通过最后几节中描述的调度功能来控制。但是哪个CPU最终执行哪个进程或线程就不说了。
程序可能还希望控制系统的这一方面的原因有很多:
- 一个线程或进程负责绝对关键的工作,在任何情况下都不得中断或阻碍其他进程或线程使用 CPU 资源取得进展。在这种情况下,特殊进程将被限制在不允许其他进程或线程使用的 CPU 中。
- 不同 CPU 访问某些资源(RAM、I/O 端口)的成本不同。NUMA(非统一内存架构)机器就是这种情况。最好应该在本地访问内存,但这个要求通常对调度程序不可见。因此,将进程或线程强制到对最常用内存-具有本地访问权限的 CPU 有助于显着提高性能。
- 在受控运行时资源分配和簿记工作(例如垃圾收集)是处理器本地的性能。如果不必保护资源免受来自不同处理器的并发访问,这有助于降低锁定成本。
迄今为止的 POSIX 标准对解决这个问题没有多大帮助。Linux 内核提供了一组接口来允许为进程指定关联集。调度程序将在关联掩码指定的 CPU 上调度线程或进程。GNU C 库定义的接口在某种程度上遵循 Linux 内核接口。
数据类型:cpu_set_t
该数据集是一个位集,其中每个位代表一个 CPU。系统的 CPU 如何映射到位集中的位取决于系统。数据类型具有固定大小;在不太可能的情况下,位数不足以描述系统的 CPU,则必须使用不同的接口。
这种类型是 GNU 扩展,在 sched.h 中定义。
为了操作位集,设置和重置位,定义了许多宏。一些宏将 CPU 编号作为参数。这里重要的是永远不要超过位集的大小。以下宏指定 cpu_set_t 位集中的位数。
宏:int CPU_SETSIZE
该宏的值是一个 cpu_set_t 对象可以处理的最大 CPU 数量。
cpu_set_t 类型应该被认为是不透明的;所有操作都应通过接下来的四个宏进行。
宏:void CPU_ZERO (cpu_set_t *set)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
该宏将 CPU 集集初始化为空集。
该宏是 GNU 扩展,在 sched.h 中定义。
宏:void CPU_SET (int cpu, cpu_set_t *set)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此宏将 cpu 添加到 CPU 集集。
cpu 参数不能有副作用,因为它被多次评估。
该宏是 GNU 扩展,在 sched.h 中定义。
宏:void CPU_CLR (int cpu, cpu_set_t *set)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此宏从 CPU 集中删除 cpu。
cpu 参数不能有副作用,因为它被多次评估。
该宏是 GNU 扩展,在 sched.h 中定义。
宏:int CPU_ISSET (int cpu, const cpu_set_t *set)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
如果 cpu 是 CPU 集的成员,则此宏返回非零值 (true),否则返回零 (false)。
cpu 参数不能有副作用,因为它被多次评估。
该宏是 GNU 扩展,在 sched.h 中定义。
CPU 位集可以从头开始构建,也可以从系统中检索当前安装的关联掩码。
函数:int sched_getaffinity (pid_t pid, size_t cpusetsize, cpu_set_t *cpuset)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 ID 为 pid 的进程或线程的 CPU 关联掩码存储在 cpuset 指向的 cpusetsize 字节长位图中。如果成功,该函数始终初始化 cpu_set_t 对象中的所有位并返回零。
如果 pid 与系统上的进程或线程不对应,或者函数由于某种其他原因而失败,则返回 -1 并设置 errno 以表示错误情况。
ESRCH
未找到具有给定 ID 的进程或线程。
EFAULT
指针 cpuset 未指向有效对象。
该函数是 GNU 扩展,在 sched.h 中声明。
请注意,使用此信息来检索不同 POSIX 线程的信息是不可移植的。必须为此提供一个单独的接口。
函数:int sched_setaffinity (pid_t pid, size_t cpusetsize, const cpu_set_t *cpuset)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数为具有 ID pid 的进程或线程安装 cpuset 指向的 cpusetsize 字节长关联掩码。如果成功,该函数将返回零,并且调度程序将来会考虑关联信息。
如果函数失败,它将返回 -1 并将 errno 设置为错误代码:
ESRCH
未找到具有给定 ID 的进程或线程。
EFAULT
指针 cpuset 未指向有效对象。
EINVAL
位集无效。这可能意味着亲和集可能不会留下处理器供进程或线程在其上运行。
该函数是 GNU 扩展,在 sched.h 中声明。
函数:int getcpu (unsigned int *cpu, unsigned int *node)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
getcpu 函数识别调用线程或进程当前正在运行的处理器和节点,并将它们写入 cpu 和节点参数指向的整数中。处理器是标识 CPU 的唯一非负整数。该节点是标识 NUMA 节点的唯一非负整数。当 cpu 或 node 为 NULL 时,不会向相应的指针写入任何内容。
成功时返回值为 0,失败时返回值为 -1。为此函数定义了以下 errno 错误条件:
ENOSYS
操作系统不支持此功能。
此函数是特定于 Linux 的,并在 sched.h 中声明。
2.4. 查询内存可用资源
Querying memory available resources
系统中可用的内存量及其组织方式通常决定了程序可以并且必须工作的方式。对于像 mmap 这样的函数,有必要了解各个内存页面的大小,并且知道有多少可用内存使程序能够为缓存等选择合适的大小。在我们深入了解这些细节之前,先介绍一下传统 Unix 系统中的内存子系统。
2.4.1. 传统 Unix 内存处理概述
Overview about traditional Unix memory handling
Unix 系统通常提供进程虚拟地址空间。这意味着内存区域的地址不必直接对应于存储数据的实际物理内存的地址。引入了一个额外的间接级别,它将虚拟地址转换为物理地址。这通常由处理器的硬件完成。
使用虚拟地址空间有几个优点。最重要的是进程隔离。系统上运行的不同进程不能直接相互干扰。没有进程可以写入另一个进程的地址空间(除非使用了共享内存,但随后需要并控制它)。
虚拟内存的另一个优点是进程看到的地址空间实际上可以大于可用的物理内存。物理内存可以通过存储在存储当前未使用的内存区域内容的外部介质上来扩展。然后,地址转换可以拦截对这些内存区域的访问,并通过将数据加载回内存来使内存内容再次可用。这个概念使得必须使用大量内存的程序必须知道可用虚拟地址空间和可用物理内存之间的区别。如果所有进程的虚拟内存的工作集大于可用的物理内存,则系统将由于内存内容从内存到存储介质的不断交换而显着减慢。这被称为“敲打”。
虚拟内存的最后一个重要方面是虚拟地址空间处理的粒度。当我们说虚拟地址处理在外部存储内存内容时,它不能逐字节地执行此操作。管理开销不允许这样做(更不用说处理器硬件了)。相反,几千个字节被一起处理并形成一个页面。每页的大小总是两个字节的幂。今天使用的最小页面大小是 4096,8192、16384 和 65536 是其他流行的大小。
2.4.2. 如何获取内存子系统的信息?
How to get information about the memory subsystem?
在几种情况下,进程看到的虚拟内存的页面大小是必不可少的。一些编程接口(例如 mmap,参见内存映射 I/O)要求用户提供调整到页面大小的信息。在 mmap 的情况下,有必要提供一个长度参数,它是页面大小的倍数。另一个有关页面大小的知识有用的地方是内存分配。如果将内存块分配为较大的块,然后由应用程序代码细分,则将较大块的大小调整为页面大小很有用。如果块的总内存需求接近(但不大于)页面大小的倍数,内核的内存处理可以更有效地工作,因为它只需要分配完全使用的内存页面。(要进行这种优化,有必要对内存分配器有所了解,这将需要为每个块本身提供一些内存,并且这种开销不能将总大小推到页面大小的倍数之上。)
页面大小传统上是一个编译时间常数。但最近处理器的发展改变了这一点。处理器现在支持不同的页面大小,它们甚至可能在同一系统上的不同进程之间有所不同。因此,系统应该在运行时询问当前页面大小,并且不应该做出任何假设(除了它是 2 的幂)。
查询页面大小的正确接口是带有参数 _SC_PAGESIZE 的 sysconf(参见 sysconf 的定义)。还有一个更旧的界面可用。
函数:int getpagesize(void)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
getpagesize 函数返回进程的页面大小。此值对于进程的运行时间是固定的,但在应用程序的不同运行中可能会有所不同。
该函数在 unistd.h 中声明。
在 System V 派生系统上广泛使用的是一种获取系统物理内存信息的方法。调用
sysconf (_SC_PHYS_PAGES)
返回系统拥有的物理内存的总页数。这并不意味着所有这些内存都可用。可以使用此信息找到
sysconf (_SC_AVPHYS_PAGES)
这两个值有助于优化应用程序。_SC_AVPHYS_PAGES 返回的值是应用程序可以在不妨碍任何其他进程的情况下使用的内存量(假设没有其他进程增加其内存使用量)。_SC_PHYS_PAGES 返回的值或多或少是工作集的硬限制。如果所有应用程序一起持续使用超过该数量的内存,系统就会出现问题。
除了这些已经描述的方法之外,GNU C 库还提供了两个函数来获取这些信息。它们在文件 sys/sysinfo.h 中声明。程序员应该更喜欢使用上述的 sysconf 方法。
函数:long int get_phys_pages (void)
Preliminary: | MT-Safe | AS-Unsafe heap lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
get_phys_pages 函数返回系统拥有的物理内存的总页数。要获得内存量,这个数字必须乘以页面大小。
这个函数是一个 GNU 扩展。
函数:long int get_avphys_pages (void)
Preliminary: | MT-Safe | AS-Unsafe heap lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
get_avphys_pages 函数返回系统拥有的可用物理内存页数。要获得内存量,这个数字必须乘以页面大小。
这个函数是一个 GNU 扩展。
2.5. 了解可用的处理器
Learn about the processors available
使用共享内存的线程或进程允许应用程序利用系统可以提供的所有处理能力。如果任务可以并行化,那么编写应用程序的最佳方式是随时运行与处理器数量一样多的进程。要确定系统可用的处理器数量,可以运行
sysconf (_SC_NPROCESSORS_CONF)
它返回操作系统配置的处理器数量。但是操作系统可能会禁用单个处理器,因此调用
sysconf (_SC_NPROCESSORS_ONLN)
返回当前在线(即可用)的处理器数量。
对于这两条信息,GNU C 库还提供了直接获取信息的函数。这些函数在 sys/sysinfo.h 中声明。
函数:int get_nprocs_conf (void)
Preliminary: | MT-Safe | AS-Unsafe heap lock | AC-Unsafe lock fd mem | See POSIX Safety Concepts.
get_nprocs_conf 函数返回操作系统配置的处理器数量。
这个函数是一个 GNU 扩展。
函数:int get_nprocs (void)
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
get_nprocs 函数返回可用处理器的数量。
这个函数是一个 GNU 扩展。
在启动更多线程之前,应该检查处理器是否还没有被过度使用。Unix 系统计算称为平均负载的东西。这是一个数字,表示正在运行的进程数。该数字是不同时间段(通常为 1、5 和 15 分钟)的平均值。
函数:int getloadavg (double loadavg[], int nelem)
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
此函数获取系统 1、5 和 15 分钟的平均负载。这些值放在 loadavg 中。getloadavg 最多会将 nelem 元素放入数组,但不会超过三个元素。返回值是写入 loadavg 的元素数,或 -1 错误。
此函数在 stdlib.h 中声明。