进程会计

    大多数 UNIX 系统都提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就会写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的 CPU 时间总量、用户 ID 和组 ID、启动时间等。函数 acct 是用来启用和禁用进程会计,唯一使用这一函数的是 accton 命令。超级用户执行一个带路径名参数的 accton 命令启用会计处理。会计记录会写到指定的文件中,在 FreeBSD 和 Mac OS X 中,该文件通常是 /var/account/acct,Linux 中是 /var/account/pacct,Solaris 中是 /var/adm/pacct。执行不带任何参数的 accton 命令则停止会计处理。
    会计记录结构定义在头文件<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)状态。

猜你喜欢

转载自aisxyz.iteye.com/blog/2393066
今日推荐