文章结构:


概念与函数

CPU时间(CPU TIME)是指当应用进程启动后,占用CPU进行计算所进行的时间绝对值,或叫时间点。如果进程进入中断、挂起、休眠等行为时,是不占用CPU的,所以CPU时间并不会跟着增加,且进程恢复运行后所获得的CPU时间值与运行中断之前的时间值是连续的,并不会因为运行的暂停而导致CPU时间的跳跃。这是和普通的日常时间值不一样的点。另一个不同的点是CPU时间并不是以秒为单位的,而是处理器的时钟周期为单位。

获取CPU时间的函数
#include <time.h>

clock_t clock(void)

该函数返回CPU时间,CPU时间并不一定会从0开始,所以要计算进程的一段代码所消耗的CPU时间记录需要在代码前后获取各自的起始和终止CPU时间,两者之差即为所消耗的CPU时间,即为处理器耗时(Processor Time)。
要将clock_t转换为秒数,需要除以宏CLOCKS_PER_SEC生成的值,即为秒数。但要注意的是由于在不同的系统中clock_tCLOCKS_PER_SEC可能为int也可能为浮点型数值,所以在计算时需要在计算过程中并其强制转换为double以保证计算结果。
另外需要注意的是clock()函数所返回的值在32位系统上每72分钟就会恢复原始值一次。
clock()如果执行错误,则会返回-1。

获取CPU时间与更详细的进程时间还有另外一个函数:
#include <sys/times.h>

clock_t times(struct tms *buf);

struct tms {
    clock_t tms_utime;  /* user time */
    clock_t tms_stime;  /* system time */
    clock_t tms_cutime; /* user time of children */
    clock_t tms_cstime; /* system time of children */
};

函数times()会将进程的处理器时间分类。在参数tms中各字段含义为:
tms_utime:用户层处理器耗时。即执行该进程的实现代码所消耗的CPU时间和。
tms_stime:系统层处理器耗时。即该进程实现的代码调用了系统内核代码,由这部分内核代码执行所消耗的CPU时间和。
tms_cutime:子进程用户层处理器耗时。即如果当前进程通过调用system()fork()函数导致产生新的子进程(数量前后可能为多个),则记录所有子进程的实现代码所消耗的CPU时间和。
tms_cstime:子进程用户层处理器耗时。即如果当前进程通过调用system()fork()函数导致产生新的子进程(数量前后可能为多个),则记录所有子进程实现的代码调用了系统内核代码,由这部分内核代码执行所消耗的CPU时间和。

函数times()所返回的值单位都是时钟滴哒(clock tick)数。要获取每秒钟的时钟滴哒可以调用函数sysconf(_SC_CLK_TCK);来获取。为了保证精度,在计算耗时时,也要在计算过程中将参与的计算值转换为double以保证计算结果。
函数times()所返回的值表示过去某个时间点到函数调用此刻的时间消耗长度。所以进程的中断、挂起、休眠等行为仍会导致返回值的持续增加。这一点是与clock()的不同所在。通常也利用这个特点来计算进程的前后自然时间耗时时长。
在计算时长方面,times()通过精确到秒以下(可能是毫秒,因为是double类型的计算),但缺点是参数buf必须不为空;difftime()由于其参数设定的原因只能精确到秒,能力是最弱的;gettimeofday()所获取的timeval则可精确到微秒,只是计算时间差值时麻烦一点。


命令Time

除了在代码中调用函数来计算各种耗时以外,还可能通过命令time来显示耗时统计。
Time命令原型如下:

time [options] command [arguments...]

options:time命令的可选参数,只接受-p,表示为按标准格式输出。如下:
time.cmd

real: 命令`date`的全部运行时间时长。
user: 命令`date`(包括其子进程,如果有的话)的用户层处理器耗时。
sys : 命令`date`(包括其子进程,如果有的话)的系统层处理器耗时。 

command:time要统计的命令。
[arguments]:command命令所接受的参数。

命令Time的输出是可以自定义的,通过设置环境变量TIMEFORMAT指定输出所要的格式化结果。
另从man手册中查看Time还具有查看CPU占用率、I/O数据量等功能(可是按文档在Mac上来操作没有成功呀…)


示例代码

接下来的代码将演示:

  1. 获取进程id,父进程id,当前线程id
  2. 线程休眠
  3. 进程生成子进程
  4. CPU时间点的获取与处理器耗时
  5. 查看进程与子进程的处理器耗时详情

获取进程id,父进程id,当前线程id

#include <sys/types.h> #include <unistd.h> #include <sys/syscall.h>

pid_t getpid(void);  // 获取进程id
pid_t getppid(void); // 获取父进程id
pid_t gettid(void){  // 获取线程id
    return syscall(SYS_gettid);
}

这里值得提一下的是getttid()是系统内核非对外公开的函数。所以这里根据其源码实现利用syscall()方法来获取线程id。


线程休眠

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

参数seconds表示线程的休眠时间,单位为秒。在指定的休眠时间内,线程会暂停工作直至时间到期或被中断。


进程生成子进程

为了测试times()中的tms_cutimetms_cstime,需要创建一个子进程,这里用到system()函数。
#include <stdlib.h>

int system(const char *command);

参数command为命令,可以是可执行程序路径也可以是系统支持的命令。system()函数会调用fork()去创建一个子进程并执行参数command。在子进程执行期间,父进程所调用的线程是被中断的,不会再进行任何操作直至等到子进程全部执行完毕。子进程所产生的日志输出也将在结束之后才会被打印出来。


CPU时间点的获取与处理器耗时

示例代码中,通过在程序的开头、进行中、结束各个阶段执行clock()来获取进程当前的CPU时间点,通过减法操作最终得到每个阶段的处理器耗时。

执行完sleep()的前后CPU时间对比可以验证CPU时间是进程所消耗处理器而产生的连续的时间集合。CPU用了多少就是多少,而与正常的时间流逝无关。
执行完system()的前后CPU时间对比可以验证子进程的CPU时间并不会被计算在父进程中,可以通过times()区分。

clock_t c_start = clock();
sleep(2); // 进程进入休眠
clock_t c_mid01 = clock();
doCopyFile(); // 进程进入繁忙的工作状态
clock_t c_mid02 = clock();
system(“”); // 生成子进程
clock_t c_end = clock();

doCopyFile()函数是复制文件的代码实现,本示例中所复制的文件大小为35M,耗时大概3.5s左右。
具体复制代码实现可以查看之前的文章:【C/C++】文件创建、打开、读、写、复制、关闭、删除等操作汇总


查看进程与子进程的处理器耗时详情

获取耗时详情的前提是要获取每秒时钟滴哒数;而详情则是通过struct tms的前后对比得出的时间差:

// process_start与后面的process_end用于计算整个进程的耗时
clock_t process_start = times(&sys_t_start);
// 获取每秒的时钟滴哒数
long tck = sysconf(_SC_CLK_TCK);
... ... ...
struct tms sys_t_end;
clock_t process_end = times(&sys_t_end);
printf("4. sys time: user time=%f, stime=%f, cutime=%f, cstime=%f \n",
       (sys_t_end.tms_utime - sys_t_start.tms_utime)/(double)tck,
       (sys_t_end.tms_stime - sys_t_start.tms_stime)/(double)tck,
       (sys_t_end.tms_cutime - sys_t_start.tms_cutime)/(double)tck,
       (sys_t_end.tms_cstime - sys_t_start.tms_cstime)/(double)tck);


printf("5. real elapsed time=%f \n", ((process_end - process_start)/(double)tck));

最后看代码的运行结果吧,如下图:
time.preview

图中,real elapsed time=9.250000time命令所打印出来的real 0m9.249s相符;
user time=3.360000 , cutime=3.380000两者之和与user 0m6.757s相符;
stime=0.120000 cstime=0.120000两者之和与sys 0m255s相符。
数值上不是绝对相等,代码内的自我计算总是会少于time的值,因为代码内是计算完了之后要打印的,这部分打印及退出的时间是代码的盲区,而time命令能统计到。

以下为实现代码:

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <sys/times.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>

pid_t gettid(void){
return syscall(SYS_gettid);
}

int main(int argc, char **argv) {
// author : sodino@qq.com
struct tms sys_t_start;
clock_t process_start = times(&sys_t_start);
clock_t c_start = clock();

times(&sys_t_start);

printf(" pid=%d ppid=%d tid=%d \n", getpid(), getppid(), gettid());

long tck = sysconf(_SC_CLK_TCK);


sleep(2);

clock_t c_mid01 = clock();
double diff = (c_mid01 - c_start)/(double)CLOCKS_PER_SEC;
printf("1. after sleep(2) consume clock time=%f \n", diff);

doCopyFile();

clock_t c_mid02 = clock();
diff = (double)(c_mid02 - c_start)/CLOCKS_PER_SEC;
printf("2. after doCopyFile() consume clock time=%f \n", diff);
// 这里会产生新的进程
system("/Users/sodino/workspace/xcode/Define/Define/t_copy.out");
clock_t c_end = clock();
diff = (double)(c_end - c_start)/CLOCKS_PER_SEC;
printf("3. after system() consume clock time=%f \n", diff);

struct tms sys_t_end;
clock_t process_end = times(&sys_t_end);
printf("4. sys time: user time=%f, stime=%f, cutime=%f, cstime=%f \n",
(sys_t_end.tms_utime - sys_t_start.tms_utime)/(double)tck,
(sys_t_end.tms_stime - sys_t_start.tms_stime)/(double)tck,
(sys_t_end.tms_cutime - sys_t_start.tms_cutime)/(double)tck,
(sys_t_end.tms_cstime - sys_t_start.tms_cstime)/(double)tck);


printf("5. real elapsed time=%f \n", ((process_end - process_start)/(double)tck));

return EXIT_SUCCESS;
}