关于UNIX:”real”、”user”和”sys”在时间输出(1)中是什么意思?

What do 'real', 'user' and 'sys' mean in the output of time(1)?

1
2
3
4
5
$ time foo
real        0m0.003s
user        0m0.000s
sys         0m0.004s
$

"real"、"user"和"sys"在时间输出中是什么意思?

当基准测试我的应用程序时,哪一个是有意义的?


实时、用户和系统进程时间统计好的。

其中一件事与另一件事不同。real是指实际运行的时间;user和sys是指仅由进程使用的CPU时间。好的。

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

  • 用户是进程内用户模式代码(内核外)所花费的CPU时间量。这只是在执行进程时使用的实际CPU时间。其他进程和阻塞的进程花费的时间不计入此数字。好的。

  • sys是进程中在内核中花费的CPU时间量。这意味着在内核内执行系统调用所花费的CPU时间,而不是在用户空间中运行的库代码。与"用户"类似,这只是进程使用的CPU时间。有关内核模式(也称为"监督"模式)和系统调用机制的简要说明,请参阅下面的内容。好的。

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

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

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

在多处理器机器上,多线程进程或进程分叉子进程的运行时间可能小于总CPU时间,因为不同的线程或进程可能并行运行。此外,报告的时间统计数据来自不同的来源,因此,对于非常短的运行任务记录的时间可能会受到舍入误差的影响,如原始海报给出的示例所示。好的。

关于内核与用户模式的简要介绍好的。

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

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

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

有关"sys"的详细信息好的。

您的代码在用户模式下不能做一些事情,比如分配内存或访问硬件(HDD、网络等)。这些都是在内核的监督下进行的,只有它才能做到。您执行的一些操作(如malloc或ocx1〔1〕或fwrite将调用这些内核函数,然后这些操作将计为'sys'时间。不幸的是,这并不像"每一个对malloc的呼叫都会被计入‘sys’时间"那么简单。对malloc的调用将对它自己进行一些处理(仍然计算在"用户"时间内),然后在调用内核中的函数的过程中的某个地方(计算在"系统"时间内)。从内核调用返回后,"用户"中会有更多的时间,然后malloc将返回到您的代码。至于什么时候发生了切换,以及在内核模式下花了多少钱…你不能说。这取决于库的实现。另外,其他看似无辜的函数也可能在后台使用malloc等类似函数,这在"sys"中会有一些时间。好的。好啊。


为了进一步讨论这个公认的答案,我只想提供另一个原因,为什么realusersys

请记住,real表示实际运行时间,而usersys值表示CPU执行时间。因此,在多核系统中,user和/或sys时间(及其总和)实际上可以超过实时。例如,在运行Java类应用程序时,我得到了一组值:

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


?实数:从开始到结束运行过程所花费的实际时间,好像是由一个带秒表的人来测量的。

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

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

Notice that sometimes user + sys might be greater than real, as
multiple processors may work in parallel.


real显示流程的总周转时间;当用户显示用户定义指令的执行时间时而sys是执行系统调用的时间!

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


最小可运行POSIX C示例

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

所有程序都可以编译和运行:

1
2
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线程)、2x Samsung M471A2K43BB1-CRC RAM(2x 16GiB)中进行了测试。

睡觉

非忙碌睡眠在usersys中都不起作用,只有real起作用。

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

1
2
3
4
5
6
7
8
#define _XOPEN_SOURCE 700
#include <stdlib.h>
#include <unistd.h>

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

Github上游。

输出如下:

1
2
3
real    0m1.003s
user    0m0.001s
sys     0m0.003s

同样适用于在IO可用时被阻止的程序。

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

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

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

Github上游。

如果你想要一秒钟的时间,就像睡眠的例子一样输出如下:

1
2
3
real    0m1.003s
user    0m0.001s
sys     0m0.003s

多线程

下面的示例对nthreads线程执行无用CPU繁重工作的niters迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#define _XOPEN_SOURCE 700
#include
#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"
", i, thread_args[i]);
    }

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

Github上游+绘图代码。

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

enter image description here

从图中我们可以看到:

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

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

    用户基本上增加了一倍,而墙保持不变。

  • 这将继续多达8个线程,这与我计算机中的多个超线程匹配。

    8点之后,Wall也开始增加,因为我们没有额外的CPU来在给定的时间内完成更多的工作!

    在这一点上,这一比率达到了顶峰。

系统与sendfile的繁重工作

我能想到的最重的系统工作负载是使用sendfile,它在内核空间上执行文件复制操作:以一种健全、安全和高效的方式复制文件。

所以我认为在内核memcpy中,这将是一个CPU密集型操作。

首先,我用以下方法初始化一个10gib的大型随机文件:

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

然后运行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define _GNU_SOURCE
#include
#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上游。

它基本上提供了预期的系统时间:

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

我还想知道time是否能区分不同进程的系统调用,因此我尝试:

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

结果是:

1
2
3
4
5
6
7
real    0m3.651s
user    0m0.000s
sys     0m1.516s

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

两个进程的sys时间与单个进程的大致相同,但wall时间更大,因为进程很可能在竞争磁盘读取访问。

因此,它似乎确实解释了哪个进程启动了一个给定的内核工作。

bash源代码

当你只在Ubuntu上执行time 操作时,它使用bash关键字,如下所示:

1
type time

输出:

1
time is a shell keyword

因此,我们在bash 4.19源代码中为输出字符串添加grep源代码:

1
git grep '"user\b'

这导致我们执行"命令c"函数time_command,它使用:

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

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

GNU coreutils源代码

如果我们称之为:

1
/usr/bin/time

然后使用gnu coreutils实现。

这一个有点复杂,但相关的来源似乎在resuse.c,它做到了:

  • 非posix bsd wait3呼叫(如果可用)
  • timesgettimeofday否则