richmon@astrovax.UUCP (Michael Richmond) (08/29/85)
I wrote the following package for my AT&T 7300, but it should work on just about any UNIX system with only minor modifications. It's not very sophisticated, keeping track only of time, memory and I/O operations on a per-user basis, but it's small and pretty easy to use. The .doc file is formatted with the -man macros. As always, let me know if there are bugs. Michael Richmond Princeton University, Astrophysics {allegra,akgua,burl,cbosgd,decvax,ihnp4,noao,princeton,vax135}!astrovax!richmon # This is a shell archive. Remove anything before this line, # then unpack it by saving it in a file and typing "sh file". # # Wrapped by richmon on Wed Aug 28 22:53:02 EDT 1985 # Contents: README makefile acct.week accton.c sa.c sa.doc echo x - README sed 's/^@//' > "README" <<'@//E*O*F README//' # To install accounting on your system, do the following: # # 0. su to super-user (and then to adm if you are cautious). # # 1. make a directory /usr/adm, owned by adm. unpack this archive # inside it and type 'make' to produce the executable programs. # # 2. change the variable CLERK in 'acct.week' to be the person in charge # of things (i.e. your login). # # 3. put the executables 'sa', 'accton' and the shell script # 'acct.week' into /usr/adm. # # 4. make the owner and group of 'sa' and 'acct.week' adm. # # 5. make the owner of 'accton' root, but the group adm. # # 6. change modes to # # sa 755 # acct.week 744 # accton 4750 # # 7. put the following line in /usr/lib/crontab (without the '#') # # 0 5 * * 2 /bin/su adm -c "/usr/adm/acct.week > /dev/null" # # 8. put the following lines in /etc/rc (again without the '#') # if you desire to have the accounting process start up # automatically on system boot: # # if test -x /usr/adm/accton # then # /usr/adm/accton /usr/adm/acctfile 1> /dev/null 2> /usr/adm/acct.err # fi # # 9. to start the accounting without reboot, su to super-user and type # # /usr/adm/accton /usr/adm/acctfile # # to halt it, su to super-user and type # # /usr/adm/accton # # @//E*O*F README// chmod u=rw,g=r,o=r README echo x - makefile sed 's/^@//' > "makefile" <<'@//E*O*F makefile//' all: accton sa accton: accton.c cc -o accton accton.c sa: sa.c cc -o sa sa.c @//E*O*F makefile// chmod u=rw,g=r,o=r makefile echo x - acct.week sed 's/^@//' > "acct.week" <<'@//E*O*F acct.week//' # this is a shell script which should be run once a week or so # via cron. first, it crunches the information contained in # the current accounting file into a more succint one which # contains the per-user stats and has a name indicating when # (i.e. which week) it was made. # next, it merges the current week's total with the those in the # file TOTALS, which will contain the sum of all per-user stats # since the time accounting is first started up (or the file # is removed). # then it empties out the old accounting file and starts up the # accounting process again. # the presence of the file 'acct.err' indicates that something went # wrong and needs to be fixed. # the person in charge of accounting CLERK=richmon ACCTFILE=/usr/adm/acctfile WEEKFILE=/usr/adm/week.`date | awk '{ print $2$3 }'` TOTALS=/usr/adm/totfile /bin/rm -f acct.err # create the file containing last week's stats /usr/adm/sa > $WEEKFILE 2> acct.err chmod 744 $WEEKFILE # merge this week's with the totals. if the file $TOTALS does # not exist, it will be created (hence the need to chmod). /usr/adm/sa -m $TOTALS chmod 744 $TOTALS # stop the accounting, empty the accounting file and restart # accounting. allow others to see, but not change, the file. /usr/adm/accton 1> /dev/null 2>> acct.err cp /dev/null $ACCTFILE chmod 744 $ACCTFILE /usr/adm/accton $ACCTFILE 1> /dev/null 2>> acct.err if test -s acct.err then # there was an error. send mail to some responsible person. /bin/mail $CLERK < acct.err else /bin/rm -f acct.err 2> /dev/null fi @//E*O*F acct.week// chmod u=rwx,g=r,o=r acct.week echo x - accton.c sed 's/^@//' > "accton.c" <<'@//E*O*F accton.c//' #include <stdio.h> #include <errno.h> /* start (or end) the system accounting routine. the should be called from /etc/rc with an argument file (such as /usr/adm/acctfile) so that whenever the system boots, accounting is enabled. all ouput goes into the (EXISTING) file named as the argument. if called with no argument, accounting is turned off. error messages are sent to the standard error - hence this should be redirected to some logical place in the /etc/rc entry. */ main(argc, argv) int argc; char *argv[]; { int i, fd; if (argc < 2) { /* turn it off if possible */ if (acct((char *) 0) < 0) { perror("Can't turn off accounting"); exit(-1); } else { printf("Accounting disabled\n"); exit(0); } } /* first create the file if it doesn't exist, since it is an error to call acct() on a non-existent file. */ if (access(argv[1]) < 0) { if (errno == ENOTDIR) { if ((fd = creat(argv[1], 033)) < 0) { perror("Can't create accounting file"); exit(-1); } else close(fd); } else { perror("Can't create accounting file"); exit(-1); } } /* now try to make the acct() call */ if (acct(argv[1]) < 0) { if (errno == EBUSY) { fprintf(stderr, "Accounting already in process\n"); exit(-1); } else { perror("Can't start accounting"); exit(-1); } } else printf("Accounting started\n"); } @//E*O*F accton.c// chmod u=rw,g=r,o=r accton.c echo x - sa.c sed 's/^@//' > "sa.c" <<'@//E*O*F sa.c//' #include <stdio.h> #include <fcntl.h> #include <sys/acct.h> /* purpose: go through the entries in /usr/adm/acctfile and sum everything up by user, producing as output the total stats per user, sorted by uid, to the standard output. this should probably be run once every week or so, since /usr/adm/acctfile can get really big quickly. a good idea is to have it run, put the output somewhere sensible (say /usr/adm/acct.week or something) and wipe the old file clean. then the accounting process can be restarted. a small shell script should do the trick. it would also be nice to have some sort of merger program to take the results from each week's dump and combine them into a cumulative file. */ #define MAXUSERS 1000 #define LINE_SIZE 75 struct acct my_acct; struct s_stats { int uid; long utime; long stime; long etime; long mem; long io; long rw; } s_array[MAXUSERS]; FILE *old_fp, *temp_fp; char *tempfile; char old_buf[100]; /* holds a line from the old accounting file, if any */ char deflt[] = "/usr/adm/acctfile"; char progname[] = "sa"; char header[] = "uid user sys elapse mem io rw"; main(argc, argv) int argc; char *argv[]; { int m_flag, err_flag, fd, nbytes; char *file, *old_file; file = deflt; m_flag = err_flag = 0; switch (argc) { case 1: break; case 2: file = argv[1]; break; case 3: if (strcmp(argv[1], "-m") == 0) { m_flag++; old_file = argv[2]; } else err_flag = 1; break; case 4: if (strcmp(argv[1], "-m") == 0) { m_flag++; old_file = argv[2]; file = argv[3]; } else err_flag = 1; break; default: err_flag = 1; break; } if (err_flag) { fprintf(stderr, "usage: sa [ -m mergefile ] [ acctfile ]\n"); exit(-1); } nbytes = sizeof(struct acct); if ((fd = open(file, O_RDONLY)) < 0) { perror(progname); exit(-1); } while (read(fd, (char *) &my_acct, nbytes) == nbytes) { process(); } if (m_flag) merge(old_file); else stats(); } /* add the information in 'my_acct' to the appropriate user's statistics. right now all we keep track of is user time, system time elapsed time memory usage characters transferred blocks read or written */ process() { int i; long expand(); s_array[(i = my_acct.ac_uid)].uid = i; s_array[i].utime += expand(my_acct.ac_utime); s_array[i].stime += expand(my_acct.ac_stime); s_array[i].etime += expand(my_acct.ac_etime); s_array[i].mem += expand(my_acct.ac_mem); s_array[i].io += expand(my_acct.ac_io); s_array[i].rw += expand(my_acct.ac_rw); } /* given a number in the form of a comp_t (defined in /usr/include/sys/acct.h), return a long value which is the closest integer. */ long expand(t) comp_t t; { long nt; unsigned int tt; /* use the top three bits as an exponent, base 8 */ tt = t >> 13; /* take the bottom 13 bits ... */ nt = t & 017777; /* and shift three bits left (into a long, so we have plenty to use) for tt times */ while (tt != 0) { tt--; nt <<= 3; } return(nt); } /* once all the ACCT structs have been read and added up, print out a line of information by user for any uid with non-zero time. */ /* return the next uid with non-zero accounting stats, or MAXUSERS if there are no more. */ next_new() { static int i; while ((s_array[i].utime == 0) && (i < MAXUSERS)) i++; return(i++); } /* convert an s_struct into a nice format for printing and return a pointer to the string. */ char * to_str(s) struct s_stats *s; { char *to_time(), *p, *malloc(); p = malloc(LINE_SIZE); sprintf(p, "%-4d %-10s %-10s %-10s %10ld %10ld %10ld", s->uid, to_time(s->utime), to_time(s->stime), to_time(s->etime), s->mem, s->io, s->rw); return(p); } /* convert a time from CPU ticks (which on this machine are 60/sec, but may vary on others - adjust to suit) to hours, minutes, seconds. return in a string with form HHHH:MM:SS . note that strange things happen if hours > 9999. */ #define SEC 60 /* may be 100 on some machines */ #define MIN 3600 /* so that these two would be 6000 */ #define HOUR 216000 /* and 360000, respectively */ char * to_time(ticks) long ticks; { char *malloc(), *p; int hour, min, sec; ticks -= (hour = (ticks / HOUR)) * HOUR; ticks -= (min = (ticks / MIN)) * MIN; sec = ticks / SEC; p = malloc(12); sprintf(p, "%4d:%02d:%02d", hour, min, sec); return(p); } /* convert from a time in the form HHHH:MM:SS to a number of CPU clock ticks. the inverse of 'to_time()'. */ long to_ticks(time) char *time; { int hour, min, sec; sscanf(time, "%d:%d:%d", &hour, &min, &sec); return(hour * HOUR + min * MIN + sec * SEC); } /* return the uid from the next line in the old accounting file, or MAXUSERS if at EOF. */ next_old() { int uid; if (fgets(old_buf, 100, old_fp) == NULL) return(MAXUSERS); if (sscanf(old_buf, "%d", &uid) < 1) return(MAXUSERS); return(uid); } /* process the next line from an old accounting file, putting the information from it into the structure passed as a parameter. return 0 if okay, 1 if end-of-file. */ parse_old(s) struct s_stats *s; { char t1[12], t2[12], t3[12]; int uid; long to_ticks(); if (sscanf(old_buf, "%d %s %s %s %ld %ld %ld", &(s->uid), t1, t2, t3, &(s->mem), &(s->io), &(s->rw)) < 7) return(1); s->utime = to_ticks(t1); s->stime = to_ticks(t2); s->etime = to_ticks(t3); return(0); } /* return a pointer to a string containing the formatted sum of a line from the old accounting file and the entry in the new table for the same uid. */ char * merge_line(uid) int uid; { char *to_str(); struct s_stats s; /* if a line from the old file is invalid, just put in the new line */ if (parse_old(&s)) return(to_str(&s_array[uid])); s.utime += s_array[uid].utime; s.stime += s_array[uid].stime; s.etime += s_array[uid].etime; s.mem += s_array[uid].mem; s.io += s_array[uid].io; s.rw += s_array[uid].rw; return(to_str(&s)); } /* print the given message and die, removing any temporary files */ die(s1, s2, s3, s4, s5) char *s1, *s2, *s3, *s4, *s5; { fprintf(stderr, "sa: %s %s %s %s %s\n", s1, s2, s3, s4, s5); unlink(tempfile); exit(-1); } /* print the statistics of the new accounting file only onto the standard output. */ stats() { char *to_str(); int i; fprintf(stdout, "%s\n", header); for (i = 0; i < MAXUSERS; i++) if (s_array[i].utime > 0) fprintf(stdout, "%s\n", to_str(&s_array[i])); } /* merge the current accounting file information with that it other one, placing the result in the other. return 0 if everything okay, 1 (and keep all files in original condition) if some problem occurs. */ merge(old) char *old; { char *mktemp(), *merge_line(); int i, old_f, new_f; struct s_stats s; tempfile = mktemp("tempXXXXXX"); if ((temp_fp = fopen(tempfile, "w+")) < 0) { fprintf(stderr, "can't open temporary file %s\n", tempfile); return(1); } if ((old_fp = fopen(old, "r")) == NULL) { fprintf(stderr, "can't open old accounting file %s\n", old); return(1); } /* remove the header from the old file */ fgets(old_buf, 100, old_fp); fprintf(temp_fp, "%s\n", header); old_f = next_old(); new_f = next_new(); while ((old_f < MAXUSERS) || (new_f < MAXUSERS)) { if (old_f == new_f) { fprintf(temp_fp, "%s\n", merge_line(old_f)); old_f = next_old(); new_f = next_new(); } else if (new_f < old_f) { fprintf(temp_fp, "%s\n", to_str(&s_array[new_f])); new_f = next_new(); } else { fprintf(temp_fp, "%s", old_buf); old_f = next_old(); } } if (unlink(old)) { fprintf("can't overwrite old file %s\n", old); return(1); } link(tempfile, old); unlink(tempfile); return(0); } @//E*O*F sa.c// chmod u=rw,g=r,o=r sa.c echo x - sa.doc sed 's/^@//' > "sa.doc" <<'@//E*O*F sa.doc//' @.TH sa 1 @.SH NAME @.PP sa - system accounting program @.SH SYNOPSIS @.IP sa [ -m mergefile ] [ acctfile ] @.SH DESCRIPTION @.PP @.I Sa takes data from @.I acctfile, a file created by @.IR acct (2), to create a summary of system use on a per-login basis. The output is a list, in ascending uid order, of uid, user, system and elapsed times (in form HH:MM:SS), memory usage, character and block I/O usage (in bytes), preceded by a one-line header. Only uids with non-zero user times are listed. @.PP If the -m flag is given, nothing is sent to the standard output. Instead, the statistics are summed with those in the file @.I mergefile, and the results placed in it. @.I Mergefile will be created if not present and filled with the current statistics. @.PP If no @.I acctfile is given, @.I /usr/adm/acctfile is used in default. @.SH OPERATION @.PP An accounting file can be created by a program which calls @.IR acct (2); if the shell script @.I /etc/rc calls such a program, it will be executed automatically every time the system is started. However, the effective user ID of the calling process must be super-user. @.SH "SEE ALSO" @.PP @.IR acct (2), acct(4) @//E*O*F sa.doc// chmod u=rw,g=r,o=r sa.doc exit 0