理解上下文切换基准测试
操作系统内核最重要的任务之一是管理进程和线程。进程是正在执行的程序,线程只是进程内存储的 CPU 状态。CPU 上下文可以是进程或线程。大多数进程只有一个线程,但一些进程,特别是服务器,有更多线程。有时服务器以外的程序也使用多线程;Netscape Navigator 就是这样一个程序的例子。这些进程和线程代表用户选择运行的程序。如果管理进程和线程的速度很慢,那么计算机的整体性能也会很慢。
Linux 倡导者自然而然地认为 Linux 比 MS Windows 提供更好的性能。Gregory Travis (greg@littlebear.com) 着手测试这一假设,测试管理进程和线程所需的时间。他的基准测试是一个简单的程序,通过生成大量除了单独循环等待之外什么都不做的进程或线程,来计时诸如 fork 或 sched_yield 之类的简单操作。他的结果在 Linux 新闻组和内核邮件列表中引起了相当大的争议。所有测试都在 200 MHz Pentium 上完成。(见表 1。)
Linux 创建进程的速度是 NT 的两倍,创建线程的速度是 NT 的三倍。进程创建比线程创建花费更长的时间(NT 为 12 倍,Linux 为 3 倍),因为进程的开销要大得多。内存映射和文件描述符表只是必须为进程创建的两件事(但线程不需要)。但是请注意,Linux 创建一个完整的进程(clone 函数)所花费的时间与 NT 仅创建线程所花费的时间大致相同(1.0 毫秒 vs. 0.9 毫秒)。
使用此基准测试和未经修改的 Linux 2.0.30 内核,NT 在上下文切换方面比 Linux 快得多,无论是在进程之间(1.9 倍)还是线程之间(3.2 倍)。由于上下文切换的发生频率远高于上下文创建,因此似乎 Linux 存在问题。但事实真是如此吗?
为什么这个简单的基准测试使 Linux 上下文切换代码比 Windows NT 慢得多?要回答这个问题,必须了解 Linux 调度器。
调度器有一个名为运行队列的列表,其中包含所有准备好运行的上下文。这些上下文没有以任何方式排序。每个上下文也有一个“优良度”,这是衡量上下文当前优先级的指标。调度器中有两个循环。第一个循环(搜索循环)从就绪队列中找到“优良度”最高的上下文,并选择该上下文运行。第二个循环(重新计算循环)在所有上下文都用完其整个时间片时重新计算“优良度”。第二个循环仅偶尔运行。列表 1 中的代码显示了调度函数的结构。
搜索循环需要少量 CPU 时间来处理每个可运行的进程,并且每次上下文切换都需要。上述基准测试显示了在存在 40 个上下文并准备好运行时的上下文切换时间。这对应于单 CPU 系统的 40 或双 CPU 系统的 20 的负载平均值。这些是非常大的负载平均值,远大于通常认为健康的负载平均值。通常,只有少数(可能是一到三个)上下文可供选择。大多数进程和线程,即使是非常活跃的进程和线程,也都在等待某些 I/O 事件发生,因此不在就绪队列中,也不考虑被选中。
即使是负载很重的机器,通常也会有大多数上下文等待 I/O 完成;这些上下文将不在运行队列中。考虑站点 http://www.winsite.com/,这是一个负载非常重的 Internet 站点,运行着大约 200 个 httpd 副本作为 Web 服务器。所有用途的进程总数约为 420 个。该机器是一台连接到三条 T1 线路的 333MHz Pentium II。
对这台机器的测量表明,在 17 小时内发生了 24,221,164 次上下文切换(每秒 400 次切换)。在这些切换中,4% 的时间内,在某一时刻有超过 10 个上下文可供选择,而只有 0.1% 的时间内有 20 个或更多。在 17 小时的持续时间内,最长的运行队列只有 36 个上下文,在 420 个可能的上下文之外。平均运行队列长度平均只有 2.5 个上下文。从某种意义上说,此基准测试代表了 Linux 的最坏情况,而平均情况,即使对于负载很重的机器来说,也要好得多。
第二个(重新计算)循环对于理解这些基准测试结果更为重要。只有当就绪队列上的每个上下文都用完其整个时间片(通常为 20 个滴答声或 0.2 秒)时,此重新计算循环才会运行。然后,重新计算循环会重新计算每个上下文的优先级。通常,此重新计算循环仅偶尔运行。大多数重新调度都是响应中断或由于 I/O 请求而完成的;因此,大多数上下文都不会使用完其整个时间片。但是,当进程或线程想要让出 CPU 时,它会调用 sched_yield,这会将其视为进程已用完其整个时间片。这样,任何调用 sched_yield 的进程的优先级都会降低,并且在相当长的一段时间内不会再次被调度。列表 2 中显示了 sched_yield 的代码。
当所有上下文都用完其整个时间片时,调度器将使用重新计算循环重新计算所有上下文的优先级。由于在此基准测试期间,所有上下文都执行一个廉价操作 (sched_yield),然后将 counter 设置为零,因此此重新计算循环的运行频率比正常情况高得多。同样,此基准测试似乎是 Linux 的最坏情况。
对于上述 Web 站点,测量结果表明,每 200 次上下文切换中只有一次需要重新计算其优先级——其他 199 次上下文切换则不需要。
一半的问题可以轻松解决;另一半则更困难。
可能没有办法使搜索循环运行得更快;它是写得很好的代码。但是,可以通过简单地将上下文按排序顺序保存在就绪队列中来消除它;那么,下一个要运行的上下文就是就绪队列头部的上下文。将就绪队列保持排序顺序将需要代码来获取添加到就绪队列的每个上下文,并将其放置在适当的位置。查找此位置所需的时间会增加调度器的复杂性。对于较大的负载平均值(意味着就绪队列上有许多上下文),可能会节省大量时间,但在较小的就绪队列的更正常情况下,没有明显的节省。
最大限度地减少重新计算循环所需的时间很容易。同样,它是写得很好的代码,不太可能得到改进,但它不需要像现在这样频繁地运行。通过更改 sched_yield,可以大大减少重新计算循环的运行频率。
对于 Linux 2.0.30,sched_yield 的行为就像让出上下文已用完整个时间片一样。相反,如果 sched_yield 的行为就像让出上下文仅使用了一个滴答声的时间片,会怎么样?将会注意到以下几个影响:
让出进程的优先级将降低一个级别,而不是暂时设置为零。
重新计算循环的运行频率将大大降低。通常,它的运行频率将降低 1/20(取决于进程优先级)。
列表 3 中显示了新的 sched_yield。将其与列表 2 中的进行比较。仅包含一行代码,但对于此基准测试,性能却得到了大幅提升。
表 2 总结了进行此更改后的性能。请注意,随着运行队列长度的增加,NT 和 Linux 调度器进行上下文切换所需的时间都会更长。Linux 开始时是更快的上下文切换器,但随着运行队列长度的增加,NT 的表现相对更好。对于运行队列长度为 20 或更短的情况(在现实生活中几乎总是如此),Linux 更好。
通过对源代码进行两行更改,可以大大改进此基准测试。但是,该基准测试可以说不能反映现实生活中的使用情况。尽管如此,只需对内核进行两行更改即可为少数用户带来显着的好处。进行此更改后,Linux 在进程和线程创建以及上下文切换的所有方面都优于 Windows NT。
本文中提及的所有列表均可通过匿名下载文件 ftp.linuxjournal.com/pub/lj/listings/issue57/2941.tgz 获取。
