8.14. Process AccountingMost UNIX systems provide an option to do process accounting. When enabled, the kernel writes an accounting record each time a process terminates. These accounting records are typically a small amount of binary data with the name of the command, the amount of CPU time used, the user ID and group ID, the starting time, and so on. We'll take a closer look at these accounting records in this section, as it gives us a chance to look at processes again and to use the fread function from Section 5.9.
A function we haven't described (acct) enables and disables process accounting. The only use of this function is from the accton(8) command (which happens to be one of the few similarities among platforms). A superuser executes accton with a pathname argument to enable accounting. The accounting records are written to the specified file, which is usually /var/account/acct on FreeBSD and Mac OS X, /var/account/pacct on Linux, and /var/adm/pacct on Solaris. Accounting is turned off by executing accton without any arguments. The structure of the accounting records is defined in the header <sys/acct.h> and looks something like typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */ struct acct { char ac_flag; /* flag (see Figure 8.26) */ 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 (clock ticks) */ comp_t ac_stime; /* system CPU time (clock ticks) */ comp_t ac_etime; /* elapsed time (clock ticks) */ 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 */ }; The ac_flag member records certain events during the execution of the process. These events are described in Figure 8.26.
The data required for the accounting record, such as CPU times and number of characters transferred, is kept by the kernel in the process table and initialized whenever a new process is created, as in the child after a fork. Each accounting record is written when the process terminates. This means that the order of the records in the accounting file corresponds to the termination order of the processes, not the order in which they were started. To know the starting order, we would have to go through the accounting file and sort by the starting calendar time. But this isn't perfect, since calendar times are in units of seconds (Section 1.10), and it's possible for many processes to be started in any given second. Alternatively, the elapsed time is given in clock ticks, which are usually between 60 and 128 ticks per second. But we don't know the ending time of a process; all we know is its starting time and ending order. This means that even though the elapsed time is more accurate than the starting time, we still can't reconstruct the exact starting order of various processes, given the data in the accounting file. The accounting records correspond to processes, not programs. A new record is initialized by the kernel for the child after a fork, not when a new program is executed. Although exec doesn't create a new accounting record, the command name changes, and the AFORK flag is cleared. This means that if we have a chain of three programsA execs B, then B execs C, and C exitsonly a single accounting record is written. The command name in the record corresponds to program C, but the CPU times, for example, are the sum for programs A, B, and C. ExampleTo have some accounting data to examine, we'll create a test program to implement the diagram shown in Figure 8.27. The source for the test program is shown in Figure 8.28. It calls fork four times. Each child does something different and then terminates. We'll run the test program on Solaris and then use the program in Figure 8.29 to print out selected fields from the accounting records. BSD-derived platforms don't support the ac_flag member, so we define the HAS_SA_STAT constant on the platforms that do support this member. Basing the defined symbol on the feature instead of on the platform reads better and allows us to modify the program simply by adding the additional definition to our compilation command. The alternative would be to use #if defined(BSD) || defined(MACOS) which becomes unwieldy as we port our application to additional platforms. We define similar constants to determine whether the platform supports the ACORE and AXSIG accounting flags. We can't use the flag symbols themselves, because on Linux, they are defined as enum values, which we can't use in a #ifdef expression. To perform our test, we do the following:
The output from step 4 follows. We have appended to each line the description of the process in italics, for the discussion later. accton e = 6, chars = 0, stat = 0: S sh e = 2106, chars = 15632, stat = 0: S dd e = 8, chars = 273344, stat = 0: second child a.out e = 202, chars = 921, stat = 0: parent a.out e = 407, chars = 0, stat = 134: F first child a.out e = 600, chars = 0, stat = 9: F fourth child a.out e = 801, chars = 0, stat = 0: F third child The elapsed time values are measured in units of clock ticks per second. From Figure 2.14, the value on this system is 100. For example, the sleep(2) in the parent corresponds to the elapsed time of 202 clock ticks. For the first child, the sleep(4) becomes 407 clock ticks. Note that the amount of time a process sleeps is not exact. (We'll return to the sleep function in Chapter 10.) Also, the calls to fork and exit take some amount of time. Note that the ac_stat member is not the true termination status of the process, but corresponds to a portion of the termination status that we discussed in Section 8.6. The only information in this byte is a core-flag bit (usually the high-order bit) and the signal number (usually the seven low-order bits), if the process terminated abnormally. If the process terminated normally, we are not able to obtain the exit status from the accounting file. For the first child, this value is 128 + 6. The 128 is the core flag bit, and 6 happens to be the value on this system for SIGABRT, which is generated by the call to abort. The value 9 for the fourth child corresponds to the value of SIGKILL. We can't tell from the accounting data that the parent's argument to exit was 2 and that the third child's argument to exit was 0. The size of the file /etc/termcap that the dd process copies in the second child is 136,663 bytes. The number of characters of I/O is just over twice this value. It is twice the value, as 136,663 bytes are read in, then 136,663 bytes are written out. Even though the output goes to the null device, the bytes are still accounted for. The ac_flag values are as we expect. The F flag is set for all the child processes except the second child, which does the execl. The F flag is not set for the parent, because the interactive shell that executed the parent did a fork and then an exec of the a.out file. The first child process calls abort, which generates a SIGABRT signal to generate the core dump. Note that neither the X flag nor the D flag is on, as they are not supported on Solaris; the information they represent can be derived from the ac_stat field. The fourth child also terminates because of a signal, but the SIGKILL signal does not generate a core dump; it only terminates the process. As a final note, the first child has a 0 count for the number of characters of I/O, yet this process generated a core file. It appears that the I/O required to write the core file is not charged to the process. Figure 8.27. Process structure for accounting exampleFigure 8.28. Program to generate accounting data#include "apue.h" int main(void) { pid_t pid; if ((pid = fork()) < 0) err_sys("fork error"); else if (pid != 0) { /* parent */ sleep(2); exit(2); /* terminate with exit status 2 */ } /* first child */ if ((pid = fork()) < 0) err_sys("fork error"); else if (pid != 0) { sleep(4); abort(); /* terminate with core dump */ } /* second child */ if ((pid = fork()) < 0) err_sys("fork error"); else if (pid != 0) { execl("/bin/dd", "dd", "if=/etc/termcap", "of=/dev/null", NULL); exit(7); /* shouldn't get here */ } /* third child */ if ((pid = fork()) < 0) err_sys("fork error"); else if (pid != 0) { sleep(8); exit(0); /* normal exit */ } /* fourth child */ sleep(6); kill(getpid(), SIGKILL); /* terminate w/signal, no core dump */ exit(6); /* shouldn't get here */ } Figure 8.29. Print selected fields from system's accounting file#include "apue.h" #include <sys/acct.h> #ifdef HAS_SA_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 #ifndef HAS_ACORE #define ACORE 0 #endif #ifndef HAS_AXSIG #define AXSIG 0 #endif 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); } int main(int argc, char *argv[]) { struct acct acdata; FILE *fp; if (argc != 2) err_quit("usage: pracct filename"); if ((fp = fopen(argv[1], "r")) == NULL) err_sys("can't open %s", argv[1]); while (fread(&acdata, sizeof(acdata), 1, fp) == 1) { printf(FMT, (int)sizeof(acdata.ac_comm), (int)sizeof(acdata.ac_comm), acdata.ac_comm, compt2ulong(acdata.ac_etime), compt2ulong(acdata.ac_io), #ifdef HAS_SA_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)) err_sys("read error"); exit(0); } |