数天前与一个学校中的朋友闲聊,对方提到了使用Hostker的经历,涉及到了一项“按CPU时间”计费的功能。
个人来说,是挺欣赏这一项收费策略的,毕竟有多少个使用虚拟主机的用户,就有多少种不同资源需求量,按照PHP对CPU资源的使用情况来计费,不仅实现公平收费,还能逼那些让资源占用多的用户占得谨慎点,比用CloudLinux的那些逼格高得多哈!
既然如此,就自己来动手实现一个。
计算CPU时间,并不难实现,Unix Like有提供这一个系统调用,所以嘛,根本不需要你自己计算……
我所知道相关的系统调用有两个,以下是他们的函数原型:
1 2 3 |
#include <sys/time.h> clock_t times(struct tms *buf); |
1 2 3 |
#include <time.h> clock_t clock(void); |
times()需要一个tms结构体的指针,tms结构体的格式如下:
1 2 3 4 5 6 |
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时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#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; } |
执行后返回以下结果:
1 2 3 4 |
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目录,执行:
1 |
./ext_skel --extname=cputime |
ext_skel会自动生成目录cputime及其需要的文件,进入cputime,编辑cputime.c。
我觉得我只需要告诉你,宏PHP_RINIT_FUNCTION(cputime)与PHP_RSHUTDOWN_FUNCTION(cputime)中的代码,分别在PHP开始解析前和PHP解析完毕后执行。
cputime.c修改完毕后,修改config.m4,找到类似的代码:
1 2 3 |
dnl PHP_ARG_ENABLE(cputime, whether to enable cputime support, dnl Make sure that the comment is aligned: dnl [ --enable-cputime Enable cputime support]) |
改成:
1 2 3 |
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计算零到十亿需要多少时间:
1 2 3 |
<?php for ($i = 0; $i < 1000000000; $i++); echo $i."\n"; |
1 2 3 |
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中。不过你最好别指望能直接搬到生产环境上用,多个进程往同一个地址同时写,数据恐怕就出问题了呢。