time(1)输出中的 “real”,“user” 和“sys”是什么意思?

$ time foo
real        0m0.003s
user        0m0.000s
sys         0m0.004s
$

时间输出中的 “真实”,“用户” 和 “sys” 是什么意思?

对我的应用进行基准测试时,哪一个有意义?

答案

实时,用户和系统处理时间统计

这些事情之一与另一件事不一样。实际是指实际经过的时间;用户和系统指仅由进程使用的 CPU 时间

  • 实际时间是挂钟时间 - 从通话开始到结束的时间。这是所有经过的时间,包括其他进程使用的时间片以及该进程花费的时间被阻塞(例如,如果它正在等待 I / O 完成)。

  • 用户的过程的用户模式代码花费(内核之外)的 CPU 时间量。这只是执行过程中使用的实际 CPU 时间。该进程花费的其他进程和时间不计入该数字。

  • Sys是进程中内核花费的 CPU 时间量。这意味着与内核代码仍在用户空间中运行的库代码相比,执行内核中的系统调用所花费的 CPU 时间。像 “用户” 一样,这只是进程使用的 CPU 时间。有关内核模式(也称为 “管理程序” 模式)和系统调用机制的简要说明,请参见下文。

User+Sys将告诉您您的进程使用了多少实际 CPU 时间。请注意,这是所有 CPU 上的情况,因此,如果进程具有多个线程(并且此进程在具有多个处理器的计算机上运行),则可能会超过Real报告的挂钟时间(通常发生)。请注意,在输出中,这些数字包括所有子进程(及其子代)的User时间和Sys时间,以及可以通过例如wait(2)waitpid(2)进行收集的所有子进程的时间,尽管底层系统调用返回了分别统计该进程及其子进程。

time (1)报告的统计数据的来源time (1)

time报告的统计信息是从各种系统调用中收集的。 “用户” 和 “系统” 来自wait (2)POSIX )或times (2)POSIX ),具体取决于特定的系统。 “真实” 是根据gettimeofday (2)调用收集的开始时间和结束时间计算得出的。取决于系统的版本,还可以按time收集各种其他统计信息,例如上下文切换的数量。

在多处理器计算机上,由于不同的线程或进程可以并行运行,因此多线程进程或派生子进程的时间可能比 CPU 总时间短。而且,报告的时间统计信息来自不同的来源,因此,对于非常短的运行任务记录的时间可能会舍入误差,如原始海报所示的示例所示。

内核与用户模式简介

在 Unix 或任何受保护的内存操作系统上, “内核” 或 “超级用户”模式是指 CPU 可以在其中运行的特权模式 。某些可能会影响安全性或稳定性的特权操作只能在 CPU 在以下情况下运行:此模式;这些操作不适用于应用程序代码。此类操作的一个示例可能是对MMU 的操纵,以获得对另一个进程的地址空间的访问。通常, 用户模式代码不能这样做(有充分的理由),尽管它可以从内核请求共享内存 ,而共享内存 可以由多个进程读取或写入。在这种情况下,共享内存是通过安全机制从内核中显式请求的,并且两个进程都必须显式连接到共享内存才能使用它。

特权模式通常称为 “内核” 模式,因为内核是由以该模式运行的 CPU 执行的。为了切换到内核模式,您必须发出一条特定的指令(通常称为trap ),该指令将 CPU 切换到以内核模式运行, 并从跳转表中的特定位置运行代码。出于安全原因,您不能切换到内核模式并执行任意代码 - 陷阱是通过地址表管理的,除非 CPU 在超级用户模式下运行,否则该地址无法写入。使用一个明确的陷阱号进行陷阱,并在跳转表中查找该地址;内核具有有限数量的受控入口点。

C 库中的 “系统” 调用(尤其是手册页的第 2 节中描述的调用)具有用户模式组件,这是您实际上从 C 程序中调用的组件。在后台,它们可能会向内核发出一个或多个系统调用以执行特定服务,例如 I / O,但它们仍然具有在用户模式下运行的代码。如果需要,也很有可能直接从任何用户空间代码向内核模式发出陷阱,尽管您可能需要编写一段汇编语言来正确设置调用寄存器。

有关 “sys” 的更多信息

有些代码是用户模式无法完成的,例如分配内存或访问硬件(HDD,网络等)。这些都是在内核的监督下完成的,仅靠内核即可做到。诸如mallocfread / fwrite类的某些操作将调用这些内核函数,然后将其计为 “sys” 时间。不幸的是,这并不像 “每次对 malloc 的调用都以'sys'时间计入” 那样简单。对malloc的调用将自己进行一些处理(仍以 “用户” 时间计),然后在其可能会调用内核中的函数的某处(以 “sys” 时间计)。从内核调用返回后,“用户” 中将有更多时间,然后malloc将返回您的代码。至于切换发生的时间,以及在内核模式下花费了多少…… 您无法确定。这取决于库的实现。同样,其他看似无害的函数也可能在后台使用malloc等,这将再次在'sys' 中占用一些时间。

为了扩展接受的答案 ,我只想提供另一个原因,为什么realuser + sys

请记住, real代表实际经过的时间,而usersys值代表 CPU 执行时间。结果,在多核系统上, user和 / 或sys时间(以及它们的总和)实际上可能超过实时时间。例如,在我正在为类运行的 Java 应用程序上,我获得了以下一组值:

real    1m47.363s
user    2m41.318s
sys     0m4.013s

真实的 :从开始到结束运行过程所花费的实际时间,就好像是人类用秒表测量的一样

用户 :计算期间所有 CPU 花费的累积时间

sys :所有 CPU 在与系统有关的任务(例如内存分配)期间花费的累积时间。

请注意,有时用户 + sys 可能大于实际值,因为多个处理器可能并行工作。

最少的可运行 POSIX C 示例

为了使事情更具体,我想用一些最少的 C 测试程序来举例说明一些极端的time情况。

可以使用以下命令编译和运行所有程序:

gcc -ggdb3 -o main.out -pthread -std=c99 -pedantic-errors -Wall -Wextra main.c
time ./main.out

并已在 Ubuntu 18.10,GCC 8.2.0,glibc 2.28,Linux 内核 4.18,ThinkPad P51 笔记本电脑,Intel Core i7-7820HQ CPU(4 核 / 8 线程),2 个 Samsung M471A2K43BB1-CRC RAM(2 个 16GiB)中进行了测试。

睡觉

不忙的睡眠不计入usersys ,仅计入real

例如,一个睡眠一秒钟的程序:

#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    sleep(1);
    return EXIT_SUCCESS;
}

GitHub 上游

输出类似:

real    0m1.003s
user    0m0.001s
sys     0m0.003s

对于在 IO 上可用而被阻止的程序也是如此。

例如,以下程序等待用户输入字符并按 Enter:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    printf("%c\n", getchar());
    return EXIT_SUCCESS;
}

GitHub 上游

而且,如果您等待大约一秒钟,它会像睡眠示例一样输出:

real    0m1.003s
user    0m0.001s
sys     0m0.003s

因此, time可以帮助您区分 CPU 和 IO 绑定的程序: “CPU 绑定” 和 “I / O 绑定” 是什么意思?

多线程

下面的例子确实niters上的无用的 CPU 沉重的工作迭代nthreads主题:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

uint64_t niters;

void* my_thread(void *arg) {
    uint64_t *argument, i, result;
    argument = (uint64_t *)arg;
    result = *argument;
    for (i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    *argument = result;
    return NULL;
}

int main(int argc, char **argv) {
    size_t nthreads;
    pthread_t *threads;
    uint64_t rc, i, *thread_args;

    /* CLI args. */
    if (argc > 1) {
        niters = strtoll(argv[1], NULL, 0);
    } else {
        niters = 1000000000;
    }
    if (argc > 2) {
        nthreads = strtoll(argv[2], NULL, 0);
    } else {
        nthreads = 1;
    }
    threads = malloc(nthreads * sizeof(*threads));
    thread_args = malloc(nthreads * sizeof(*thread_args));

    /* Create all threads */
    for (i = 0; i < nthreads; ++i) {
        thread_args[i] = i;
        rc = pthread_create(
            &threads[i],
            NULL,
            my_thread,
            (void*)&thread_args[i]
        );
        assert(rc == 0);
    }

    /* Wait for all threads to complete */
    for (i = 0; i < nthreads; ++i) {
        rc = pthread_join(threads[i], NULL);
        assert(rc == 0);
        printf("%" PRIu64 " %" PRIu64 "\n", i, thread_args[i]);
    }

    free(threads);
    free(thread_args);
    return EXIT_SUCCESS;
}

GitHub 上游 + 绘图代码

然后,在我的 8 个超线程 CPU 上,针对固定的 10 ^ 10 迭代,将 wall,user 和 sys 绘制为线程数的函数:

在此处输入图片说明

绘制数据

从图中可以看到:

  • 对于 CPU 密集型单核应用程序,wall 和 user 大致相同

  • 对于 2 核,用户大约是 2 倍墙,这意味着用户时间是在所有线程中计算的。

    用户基本上翻了一番,而墙保持不变。

  • 这最多可以持续 8 个线程,这与我计算机中超线程的数量相匹配。

    8 点之后,墙也开始增加,因为我们没有任何额外的 CPU 在给定的时间内进行更多工作!

    此时的比率平稳。

使用sendfile系统繁重的工作

我能想到的最重的 sys 工作负载是使用sendfile ,它在内核空间上执行文件复制操作: 以理智,安全,高效的方式复制文件

所以,我想到的是,这个在内核memcpy将是 CPU 密集型操作。

首先,我使用以下命令初始化一个较大的 10GiB 随机文件:

dd if=/dev/urandom of=sendfile.in.tmp bs=1K count=10M

然后运行代码:

#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv) {
    char *source_path, *dest_path;
    int source, dest;
    struct stat stat_source;
    if (argc > 1) {
        source_path = argv[1];
    } else {
        source_path = "sendfile.in.tmp";
    }
    if (argc > 2) {
        dest_path = argv[2];
    } else {
        dest_path = "sendfile.out.tmp";
    }
    source = open(source_path, O_RDONLY);
    assert(source != -1);
    dest = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    assert(dest != -1);
    assert(fstat(source, &stat_source) != -1);
    assert(sendfile(dest, source, 0, stat_source.st_size) != -1);
    assert(close(source) != -1);
    assert(close(dest) != -1);
    return EXIT_SUCCESS;
}

GitHub 上游

基本上可以提供预期的系统时间:

real    0m2.175s
user    0m0.001s
sys     0m1.476s

我也很好奇,看看time是否可以区分不同进程的系统调用,所以我尝试:

time ./sendfile.out sendfile.in1.tmp sendfile.out1.tmp &
time ./sendfile.out sendfile.in2.tmp sendfile.out2.tmp &

结果是:

real    0m3.651s
user    0m0.000s
sys     0m1.516s

real    0m4.948s
user    0m0.000s
sys     0m1.562s

两者的系统时间与单个进程的时间大致相同,但是挂墙时间较长,因为进程可能争用磁盘读取访问权限。

因此,似乎实际上是在考虑哪个进程启动了给定的内核工作。

Bash 源代码

在 Ubuntu 上time <cmd>时,它使用 Bash 关键字,如下所示:

type time

输出:

time is a shell keyword

因此我们在 Bash 4.19 源代码中为输出字符串使用 grep 源代码:

git grep '"user\b'

这导致我们执行execute_cmd.c函数time_command ,它使用:

  • gettimeofday()getrusage()如果两者均可用)
  • times()否则

所有这些都是Linux 系统调用POSIX 函数

GNU Coreutils 源代码

如果我们称其为:

/usr/bin/time

然后使用 GNU Coreutils 实现。

这个有点复杂,但是相关的资源似乎在resuse.c 上 ,它确实:

  • 非 POSIX BSD wait3调用(如果有)
  • timesgettimeofday否则

实际显示流程的总周转时间;用户显示用户定义指令的执行时间,而 Sys 显示系统调用的执行时间!

实时还包括等待时间(I / O 等的等待时间)