会计记录结构定义在头文件<sys/acct.h>中,虽然每种系统的实现各不相同,但会计记录样式基本如下:
#include <sys/acct.h> typedef u_short comp_t; // 3-bit base 8 exponent; 13-bit fraction type acct{ char ac_flag; // flag, see below char ac_stat; // termination status(signal & core flag only) // (Solaris only) uid_t ac_uid; // real user ID gid_t ac_gid; // real group ID dev_t ac_tty; // controlling terminal time_t ac_btime; // starting calendar time comp_t ac_utime; // user CPU time comp_t ac_stime; // system CPU time comp_t ac_etime; // elapsed time comp_t ac_mem; // average memory usage comp_t ac_io; // bytes transferred (by read and write) // "blocks" on BSD systems comp_t ac_rw; // blocks read or written (not present on BSD systems) char ac_comm[8]; // command name: [8] for Solaris, [10] for Mac OS X, // [16] for FreeBSD and [17] for Linux };
其中 ac_flag 成员记录了进程在执行期间的如下事件。
会计记录所需的各个数据(各 CPU 时间、传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时初始化。进程终止时写一个会计记录。这产生两个结果。
(1)不能获取永远不终止的进程的会计记录,如 init 进程和内核守护进程等。
(2)在会计文件中记录的顺序对应于进程终止的顺序,而不是它们启动的顺序。为了确定启动顺序,需要读取全部会计文件,然后按启动日历时间进行排序。
会计记录对应于进程而不是程序。在 fork 之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。虽然 exec 并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK 标志则被清除。这意味着,如果一个进程顺序执行了 3 个程序(A exec B、B exec C,最后是 C exit),只会写一个会计记录,在该记录中的命令名对应与 C,但 CPU 时间是 A、B 和 C 之和。
下面这个程序 acctDemo 可用来生成会计数据。它按下图调用了 4 次 frok,每个子进程做不同的事情,然后终止。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(void){ pid_t pid; if((pid=fork()) < 0){ printf("fork 1 error\n"); }else if(pid != 0){ // parent sleep(2); exit(2); // terminate with exit status 2 } if((pid=fork()) < 0){ printf("fork 2 error\n"); }else if(pid != 0){ // first child sleep(4); abort(); // terminate with core dump } if((pid=fork()) < 0){ printf("fork 3 error\n"); }else if(pid != 0){ // second child execl("/bin/dd", "if=/etc/passwd", "of=/dev/null", NULL); exit(7); // shouldn't get here } if((pid=fork()) < 0){ printf("fork 4 error\n"); }else if(pid != 0){ // third child sleep(8); exit(0); // normal exit } sleep(6); // fourth child kill(getpid(), SIGKILL); // terminate w/signal, no core dump exit(6); // shouldn't get here }
然后可使用下面这个程序 printAcct 来打印运行上面程序后产生的会计数据。
#include <stdio.h> #include <stdlib.h> #include <sys/acct.h> //#define BSD // 如果是 BSD 系统就取消该行注释 //#define LINUX // 如果是 Linux 系统就取消该行注释 //#define HAS_AC_STAT // 如果 acct 结构有 ac_stat 这个成员就取消该行注释 //#define HAS_ACORE // 如果 ac_flag 标记支持 ACORE 选项就取消该行注释 //#define HAS_AXSIG // 如果 ac_flag 标记支持 AXSIG 选项就取消该行注释 #if defined(BSD) // different structure in FreeBSD #define acct acctv2 #define ac_flag ac_trailer.ac_flag #define FMT "%-*.*s e = %.0f, chars = %.0f, %c %c %c %c\n" #elif defined(HAS_AC_STAT) #define FMT "%-*.*s e = %6ld, chars = %7ld, stat = %3u: %c %c %c %c\n" #else #define FMT "%-*.*s e = %6ld, chars = %7ld, %c %c %c %c\n" #endif #if defined(LINUX) #define acct acct_v3 // different structure in Linux #endif #if !defined(HAS_ACORE) #define ACORE 0 #endif #if !defined(HAS_AXSIG) #define AXSIG 0 #endif #if !defined(BSD) static unsigned long compt2ulong(comp_t comptime){ // convert comp_t to unsigned long unsigned long val; int exp; val = comptime & 0x1fff; // 13-bit fraction exp = (comptime >> 13) & 7; // 3-bit exponent (0-7) while(exp-- > 0) val *= 8; return val; } #endif int main(int argc, char *argv[]){ struct acct acdata; FILE *fp; if(argc != 2){ printf("usage: %s filename\n", argv[0]); exit(1); } if((fp=fopen(argv[1], "r")) == NULL){ printf("can't open %s\n", argv[1]); exit(2); } while(fread(&acdata, sizeof(acdata), 1, fp) == 1){ printf(FMT, (int)sizeof(acdata.ac_comm), (int)sizeof(acdata.ac_comm), acdata.ac_comm, #if defined(BSD) acdata.ac_etime, acdata.ac_io, #else compt2ulong(acdata.ac_etime), compt2ulong(acdata.ac_io), #endif #if defined(HAS_AC_STAT) (unsigned char)acdata.ac_stat, #endif acdata.ac_flag & ACORE ? 'D': '-', acdata.ac_flag & AXSIG ? 'X': '-', acdata.ac_flag & AFORK ? 'F': '-', acdata.ac_flag & ASU ? 'S': '-'); } if(ferror(fp)) printf("read error\n"); exit(0); }
BSD 派生的平台不支持 ac_stat 成员,所以我们在支持该成员的平台上定义了 HAS_AC_STAT 产量。同理,我们还定义了类似的常量以判断该平台是否支持 ACORE 和 AXSIG 会计标志,不能直接使用这两个标志符号,因为它们在 Linux 中被定义为 enum 类型值,而在 #ifdef 中不能使用此种类型值。
为进行测试,需要执行下列操作步骤。
(1)成为超级用户,用 accton 命令启用会计处理。注意,当此命令结束时,会计处理已经启用,因此会计文件中的第一个记录应来自这一命令。
(2)终止超级用户 shell,运行上面的 acctDemo 程序,这会追加 6 个记录到会计文件中(超级用户 shell 一个、父进程一个、4 个子进程各一个)。注意,第二个子进程中的 execl 并不创建一个新进程,所以只有一个会计记录。
(3)成为超级用户,停止会计处理。因为在 accton 命令终止时已经停止会计处理,所以不会在会计文件中增加一个记录。
(4)运行 printAcct 程序,从会计文件中选出字段并打印。
整个流程在 Solaris 上的运行结果如下:
$ ls -l /var/adm/pacct -rw-r--r--. 1 root root 0 9月 13 23:57 /var/adm/pacct $ $ su 密码: # accton /var/adm/pacct # 打开进程会计 # ./acctDemo.out # accton # 关闭进程会计 # # ./printAcct.out /var/adm/pacct accton e = 1, chars = 336, stat = 0: - - - S sh e = 1550, chars = 20168, stat = 0: - - - S dd e = 2, chars = 1585, stat = 0: - - - - # 第二个子进程 acctDemo.out e= 202, chars= 0, stat = 0: - - - - # 父进程 acctDemo.out e= 420, chars= 0, stat = 134: - - F - # 第一个子进程 acctDemo.out e= 600, chars= 0, stat = 0: - - F - # 第四个子进程 acctDemo.out e= 801, chars= 0, stat = 0: - - F - # 第三个子进程
注意,ac_stat 成员并不是真正的终止状态,而只是其中的一部分。如果进程异常终止,则此字节包含的信息只是 core 标志位(一般是最高位)以及信号编号数(一般是低 7 位);如果进程正常终止,则从会计文件不能得到进程的退出(exit)状态。