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

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

realusersys在时间输出中意味着什么? 在我的应用程序基准测试时,哪一个是有意义的?

593313 次浏览

实际、用户和系统处理时间统计

其中一个与另一个不同。Real指的是实际经过的时间;User和Sys指的是使用的CPU时间只有通过过程。

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

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

  • Sys是进程在内核中花费的CPU时间。这意味着在内核中执行系统调用所花费的CPU时间,而不是库代码,库代码仍在用户空间中运行。与'user'一样,这只是进程使用的CPU时间。有关内核模式(也称为'主管'模式)和系统调用机制的简要描述,请参阅下文。

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

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

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

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

关于Kernel vs. User模式的简短入门

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

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

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

更多关于“sys”

有些事情是代码在用户模式下无法完成的,比如分配内存或访问硬件(硬盘、网络等)。这些是在内核的监督下,只有内核才能完成。像mallocfread/fwrite这样的操作会调用这些内核函数,然后这些操作将被计为“sys”时间。不幸的是,它并不像“每次调用malloc都会在“sys”时间内计数”那么简单。对malloc的调用会自己做一些处理(仍然在“用户”时间内计数),然后在此过程中的某个地方它可能会调用内核中的函数(以“sys”时间计数)。从内核调用返回后,会有更多的时间在'user'中,然后malloc会返回到你的代码中。至于开关什么时候发生,以及其中有多少是在内核模式下花费的……你不能说。这取决于库的实现。此外,其他看似无辜的函数也可能在后台使用malloc等,这样在'sys'中也会有一些时间。

为了扩展接受的答案,我只是想提供另一个原因real=user+sys

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

real    1m47.363suser    2m41.318ssys     0m4.013s

Real显示进程的总周转时间;而User显示用户定义指令的执行时间而Sys是执行系统调用的时间!

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

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

用户:计算过程中所有CPU花费的累计时间

sys:所有CPU在内存分配等系统相关任务期间花费的累积时间。

请注意,有时user+sys可能大于real,因为多个处理器可以并行工作。

最小可运行POSIX C示例

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

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

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

并已在Ubuntu 18.10,GCC 8.2.0,glibc 2.28,Linux内核4.18,ThinkPadP51笔记本电脑,英特尔酷睿i7-7820HQ CPU(4核/8线程),2x三星M471A2K43BB1-CRC RAM(2x 16GiB)中进行了测试。

sleep系统调用

sleep系统调用完成的非忙睡眠仅计入real,但不计入usersys

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

#define _XOPEN_SOURCE 700#include <stdlib.h>#include <unistd.h>
int main(void) {sleep(1);return EXIT_SUCCESS;}

github上游

输出如下:

real    0m1.003suser    0m0.001ssys     0m0.003s

对于在IO上阻塞的程序变得可用也是如此。

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

#include <stdio.h>#include <stdlib.h>
int main(void) {printf("%c\n", getchar());return EXIT_SUCCESS;}

github上游

如果你等待大约一秒钟,它会像睡眠示例一样输出如下内容:

real    0m1.003suser    0m0.001ssys     0m0.003s

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

多线程

以下示例在nthreads线程上执行niters次无用的纯CPU绑定工作:

#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上游+绘图代码

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

输入图片描述

情节数据

从图表中,我们可以看到:

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

  • 对于2个核心,用户大约是2x墙,这意味着用户时间在所有线程中计数。

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

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

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

    比率在这一点上趋于平稳。

请注意,这个图非常清晰和简单,因为工作纯粹是CPU受限的:如果它是内存受限的,那么我们会在内核较少的情况下更早地降低性能,因为内存访问将是一个瓶颈,如术语“CPU绑定”和“I/O绑定”是什么意思?所示

快速检查wall

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.175suser    0m0.001ssys     0m1.476s

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

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

结果是:

real    0m3.651suser    0m0.000ssys     0m1.516s
real    0m4.948suser    0m0.000ssys     0m1.562s

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

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

bash源代码

当你在Ubuntu上只做time <cmd>时,它使用Bash关键字,如图所示:

type time

其产出:

time is a shell keyword

所以我们在Bash 4.19源代码中为输出字符串提供grep source:

git grep '"user\b'

这将我们引向execute_cmdc函数time_command,它使用:

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

它们都是Linux系统调用POSIX函数

GNU Coreutils源代码

如果我们称之为:

/usr/bin/time

然后它使用GNU Coreutils实现。

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

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

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

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

并已在Ubuntu 18.10,GCC 8.2.0,glibc 2.28,Linux内核4.18,ThinkPadP51笔记本电脑,英特尔酷睿i7-7820HQ CPU(4核/8线程),2x三星M471A2K43BB1-CRC RAM(2x 16GiB)中进行了测试。

睡眠

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

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

#define _XOPEN_SOURCE 700#include <stdlib.h>#include <unistd.h>
int main(void) {sleep(1);return EXIT_SUCCESS;}

github上游

输出如下:

real    0m1.003suser    0m0.001ssys     0m0.003s

对于在IO上阻塞的程序变得可用也是如此。

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

#include <stdio.h>#include <stdlib.h>
int main(void) {printf("%c\n", getchar());return EXIT_SUCCESS;}

github上游

如果你等待大约一秒钟,它会像睡眠示例一样输出如下内容:

real    0m1.003suser    0m0.001ssys     0m0.003s

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

简单来说,我喜欢这样想:

  • real是运行命令所需的实际时间(就像你用秒表计时一样)

  • usersysCPU为执行命令所做的工作量。这个“工作”以时间单位表示。

一般来说:

  • userCPU运行命令代码所做的工作量
  • sysCPU必须做多少工作来处理“系统开销”类型的任务(例如分配内存、文件I/O等)以支持正在运行的命令

由于最后两次计算的是完成的“工作”,因此它们不包括线程可能花费的等待时间(例如等待另一个进程或磁盘I/O完成)。

然而,real是实际运行时间的度量,而不是“工作”,因此它确实包括等待所花费的任何时间。

我想提一些其他场景,当实时比user+sys大得多时。我创建了一个简单的服务器,它会在很长时间后响应

real 4.784user 0.01ssys  0.01s

问题是在这种情况下,进程等待不在用户站点上也不在系统中的响应。

当您运行find命令时也会发生类似的情况。在这种情况下,时间主要花在从SSD请求和获取响应上。

必须提到,至少在我的AMD Ryzen CPU上,在多线程程序(或用-O3编译的单线程程序)中,user总是比real大。

eg.

real    0m5.815suser    0m8.213ssys 0m0.473s