C/CPP, PHP

[PHP模块开发]获取单次请求所耗的CPU时间

数天前与一个学校中的朋友闲聊,对方提到了使用Hostker的经历,涉及到了一项“按CPU时间”计费的功能。

个人来说,是挺欣赏这一项收费策略的,毕竟有多少个使用虚拟主机的用户,就有多少种不同资源需求量,按照PHP对CPU资源的使用情况来计费,不仅实现公平收费,还能逼那些让资源占用多的用户占得谨慎点,比用CloudLinux的那些逼格高得多哈!

 

既然如此,就自己来动手实现一个。

计算CPU时间,并不难实现,Unix Like有提供这一个系统调用,所以嘛,根本不需要你自己计算……

我所知道相关的系统调用有两个,以下是他们的函数原型:

#include <sys/time.h>

clock_t times(struct tms *buf);
#include <time.h>

clock_t clock(void);

times()需要一个tms结构体的指针,tms结构体的格式如下:

           struct tms {
               clock_t tms_utime;  /* CPU在用户态所消耗时间 */
               clock_t tms_stime;  /* CPU在核心态所消耗时间 */
               clock_t tms_cutime; /* 所有子线程还是子进程用户态所消耗的时间 */
               clock_t tms_cstime; /* 所有子线程还是子进程核心态所消耗的时间 */
           };

tms_cutime和tms_cstime不太清楚是计算子线程还是子进程,毕竟目前我还没接触到多线程开发,不过一般情况下PHP还是使用单进程单线程模式的,只需要用到tms_utime和tms_stime。

tms结构体中的值的单位并非为秒,内核计时是有一个频率的,因此除以该计时频率的值,才能得到以秒为单位的值,使用sysconf(_SC_CLK_TCK)可以获取到该值。

times函数的返回值是一个过去的时间,我看了下,好像除以一百才是秒,所以两次调用的返回值之差除以一百可以得到自然流逝的时间。不过man page里面说The return value may overflow the possible range of type clock_t.,意思是返回值可能溢出。

 

clock()不需要任何参数,直接返回用户态和核心态使用的时间总和,同样,返回值的单位不是秒,需要除以一个常量: CLOCKS_PER_SEC。

 

我们来看看C计算从零到十亿需要多少CPU时间:

#include <stdio.h>
#include <sys/times.h>
#include <time.h>
#include <unistd.h>

int main(void) {
    int i;
    struct tms cpu_time;
    clock_t clock_start, clock_end, clock_time, clock_tick = sysconf(_SC_CLK_TCK);
    clock_start = times(NULL);
    for (i = 0; i < 1000000000; i++);
    clock_end = times(&cpu_time);
    clock_time = clock();
    printf("i = %d\n", i);
    printf("clock_start: %lf, clock_end: %lf, clock_flew: %lf\n", clock_start / 100., clock_end / 100., (clock_end - clock_start) / 100.);
    printf("User Time: %lf, System Time: %lf\n", 1. * cpu_time.tms_utime / clock_tick, 1. * cpu_time.tms_stime / clock_tick);
    printf("CPU Time by clock(): %lf\n", 1. * clock_time / CLOCKS_PER_SEC);
    return 0;
}

执行后返回以下结果:

i = 1000000000
clock_start: 17181484.780000, clock_end: 17181487.990000, clock_flew: 3.210000
User Time: 3.220000, System Time: 0.020000
CPU Time by clock(): 3.245232

times()函数获取到用户态耗时3.22秒,核心态耗时0.02秒,总CPU时间3.24秒,自然时间流逝3.21秒。

clock()函数取到3.25秒,和times()有点差距,毕竟调用times()也要耗时。

至于自然时间流逝比总CPU耗时短的问题,有两种情况。

第一种是我们获取开始时间并非真正在程序启动时就获取,而CPU时间的计算是内核在程序启动一刻开始计时的,程序启动到我们获取起始时间之间是有时间差的。

第二种是多线程,如果在自然时间一秒内同时使用满两个CPU核心,那么自然时间流逝是一秒,CPU时间就是两秒,不过这里是单线程编程,应该不涉及这种情况。

 

知道了如何调用这些API,接下来就是要弄成PHP模块了。

PHP的模块开发也不难,PHP已经为你准备好了一切,我来教你如何生成基本模块结构。

假设我们的模块名定为cputime。

下载PHP的源码,进入源码根目录下的ext目录,执行:

./ext_skel --extname=cputime

ext_skel会自动生成目录cputime及其需要的文件,进入cputime,编辑cputime.c。

我觉得我只需要告诉你,宏PHP_RINIT_FUNCTION(cputime)与PHP_RSHUTDOWN_FUNCTION(cputime)中的代码,分别在PHP开始解析前和PHP解析完毕后执行。

 

cputime.c修改完毕后,修改config.m4,找到类似的代码:

dnl PHP_ARG_ENABLE(cputime, whether to enable cputime support,
dnl Make sure that the comment is aligned:
dnl [  --enable-cputime           Enable cputime support])

改成:

PHP_ARG_ENABLE(cputime, whether to enable cputime support,
dnl Make sure that the comment is aligned:
[  --enable-cputime           Enable cputime support])

这样就能通过phpize生成模块了。

 

一切完毕后,按照正常使用PHP模块的方式编译,加载。

 

我们来看看PHP计算零到十亿需要多少时间:

<?php
	for ($i = 0; $i < 1000000000; $i++);
	echo $i."\n";
root@debian:~# php one_billion.php 
1000000000
uid: 0, gid: 0, euid: 0, egid: 0, Natural time: 107.110000 s, User time: 107.060000000 s, System time: 0.060000000 s, Total by clock(): 107.138508 s

一百零七秒多,人民群众喜闻乐见。

 

如果你想说你不会改cputime.c,这里还有一个成品: https://coding.net/u/yzs/p/PHP-CPUTIME-PRINT/git,可以把每次页面的时间记录追加到/tmp/uid_用户id_php_cputime中。不过你最好别指望能直接搬到生产环境上用,多个进程往同一个地址同时写,数据恐怕就出问题了呢。

102 Posts

自信、努力、活出精彩;以前未所见的颜色,绘大千世界!
View all posts

Leave a reply

Your email address will not be published. Required fields are marked *