|
|
Theperfmeter
tool is well known, but few people know how it works or how easy it is to get at the data it displays. It is a trivial job to generate the code required to read the underlying remote procedure call. The data returned is more extensive than you might think and is quite useful. A new interface to this data from the SE performance toolkit and a remotevmstat
clone are described. (3,600 words)
Mail this article to a friend |
perfmeter
work, and exactly what data does it
collect for display?
perfmeter
makes a remote procedure call to the
rpc.rstatd
daemon on either the local or a remote
machine.
The rpc.rstatd
daemon is an RPC server that obtains
performance data by using kstat
, returning the data in
an RPC message. The rup(1)
and perfmeter(1)
commands monitor local and remote system activity by using the
rstatd
interface. SunNet Manager also uses
rpc.rstatd
to monitor host performance. Two versions of
the protocol are currently defined in /usr/include/rpcsvc/rstat.x
.
Version 3 is used by SunOS 4; Version 4, shown below, allows a variable
number of CPU states and a variable number of disks to be defined. The
information returned is the CPU user, system, wait, idle counters, the
total number of operations on each disk, pages paged in and out, pages
swapped in and out, the number of interrupts, total packet, error and
collision counts summed across all interfaces, context switch count,
average run queue lengths for 1-, 5-, and 15-minute averages (each run
queue value is shifted by 8 bits, so divide by 256.0 and report it in
floating point), and a microsecond resolution timestamp for the current
time and the time the system was booted.
|
|
|
|
RPC rstatd
returned data structure:
struct statsvar { struct { u_int cp_time_len; int *cp_time_val; } cp_time; struct { u_int dk_xfer_len; int *dk_xfer_val; } dk_xfer; u_int v_pgpgin; u_int v_pgpgout; u_int v_pswpin; u_int v_pswpout; u_int v_intr; int if_ipackets; int if_ierrors; int if_opackets; int if_oerrors; int if_collisions; u_int v_swtch; long avenrun[3]; rstat_timeval boottime; rstat_timeval curtime; };
You can generate example code that collects this data, and you can use
rpcgen(1)
to generate a makefile:
% cp /usr/include/rpcsvc/rstat.x . % rpcgen -a rstat.x % ls *rstat* makefile.rstat rstat.x rstat_clnt.c rstat_svc.c rstat.h rstat_client.c rstat_server.c rstat_xdr.c % make -f makefile.rstat rstat_client
You now have a program that gets the data from the system you specify
but does nothing with it. We need to modify the main program in
rstat_client.c
to do something with
the returned data structure so that we can see the results.
The following code sample accomplishes this. Note that I deleted
code for handling older versions of the rstat
protocol and
added printf
statements, as shown by the highlighted code
below. Everything else was generated by rpcgen
:
/* * This is sample code generated by rpcgen. * These are only templates. Use them as a * guideline for developing your own functions. */ #include "rstat.h" #include#include /* getenv, exit */ /* * Copyright (c) 1985, 1990, 1991 by Sun Microsystems, Inc. */ /* from rstat.x */ void rstatprog_4(host) char *host; { int i; CLIENT *clnt; statsvar *rs; char * rstatproc_stats_4_arg; u_int *result_2; char * rstatproc_havedisk_4_arg; #ifndef DEBUG clnt = clnt_create(host, RSTATPROG, RSTATVERS_VAR, "netpath"); if (clnt == (CLIENT *) NULL) { clnt_pcreateerror(host); exit(1); } #endif /* DEBUG */ rs = rstatproc_stats_4((void *)&rstatproc_stats_4_arg, clnt); if (rs == (statsvar *) NULL) { clnt_perror(clnt, "call failed"); } result_2 = rstatproc_havedisk_4((void *)&rstatproc_havedisk_4_arg, clnt); if (result_2 == (u_int *) NULL) { clnt_perror(clnt, "call failed"); } printf("rstat information from %s at: %s", host, ctime(&rs->curtime.tv_sec)); printf("booted at %s", ctime(&rs->boottime.tv_sec)); printf("pgpgin: %d pgpgout: %d pswpin: %d pswpout: %d\n", rs->v_pgpgin, rs->v_pgpgout, rs->v_pswpin, rs->v_pswpout); printf("intr: %d swtch: %d avenrun: %3.1f %3.1f %3.1f\n", rs->v_intr, rs->v_swtch, rs->avenrun[0]/256.0, rs->avenrun[1]/256.0, rs->avenrun[2]/256.0); printf("ipackets: %d ierrors: %d opackets: %d oerrors: %d \ collisions: %d\n", rs->if_ipackets, rs->if_ierrors, rs->if_opackets, rs->if_oerrors, rs->if_collisions); /* CPU states are 0=USER 1=WAIT 2=KERNEL 3=IDLE - weird... */ printf("usr: %d sys: %d wio: %d idle: %d\n", rs->cp_time.cp_time_val[0], rs->cp_time.cp_time_val[2], rs->cp_time.cp_time_val[1], rs->cp_time.cp_time_val[3]); for(i=0; i < rs->dk_xfer.dk_xfer_len; i++) { printf("disk: %d xfers: %d\n", i, rs->dk_xfer.dk_xfer_val[i]); } #ifndef DEBUG clnt_destroy(clnt); #endif /* DEBUG */ } main(argc, argv) int argc; char *argv[]; { char *host; if (argc < 2) { \x11\x11printf("usage: %s server_host\n", argv[0]); \x11\x11exit(1); } host = argv[1]; rstatprog_4(host); }
The raw data printout is shown below. The values are counters, so to
get per-second rates, you would have to take measurements twice and use
the differences over a time interval. The current time does include a
microsecond resolution component, so accurate time differences are
available. Some counters may wrap around, as intr
has in
the example, but differences will still be valid.
% rstat_client localhost rstat information from localhost at: Fri Dec 19 09:21:53 1997 booted at Sat Nov 29 11:42:36 1997 pgpgin: 186806 pgpgout: 105086 pswpin: 24 pswpout: 145 intr: -25232137 swtch: 56685160 avenrun: 0.0 0.0 0.0 ipackets: 694345 ierrors: 0 opackets: 736283 oerrors: 0 collisions: 1 usr: 1184180 sys: 351773 wio: 280473 idle: 127491765 disk: 0 xfers: 1 disk: 1 xfers: 225406 disk: 2 xfers: 162365 disk: 3 xfers: 37496
The disk data is hard to map to named disk devices. I compared it
with counters obtained from iostat
to check this; the order
is the same:
% iostat -xI extended disk statistics disk r/i w/i Kr/i Kw/i wait actv svc_t %w %b fd0 1.0 0.0 0.0 0.0 0.0 0.0 7.8 0 0 sd0 118384.0 108737.0 841384.5 1344148.0 0.2 0.1 477.7 0 1 sd1 94500.0 68373.0 615959.5 742512.0 0.0 0.0 53.1 0 1 sd5 28362.0 9144.0 155954.5 78172.0 0.0 0.0 40.8 0 0
Figure 1: Example perfmeter output
|
The data shown by perfmeter
in Figure 1 summarizes all
this information by adding together CPU user and system time, all
paging activity, packets in and out, and all disk I/O counters.
Perfmeter
can log the data it is displaying to a file and
has more options than most users realize, so take a look at the manual
page.
Interfacing the SE Toolkit to the rstat
RPC data
So far, we have code in C that gets at a new source of performance
information. It is an interesting data source because it is enabled by
default on all Solaris (and most other Unix) systems, and you don't
need to install anything or be able to login to the machine to monitor
it. I like to have all my performance data available in the SE
language, so I spent a few hours working out how. It also acts as a
useful tutorial in how to interface C code to SE scripts.
There are three components that need to be built. First, the existing C code must be turned into a set of callable routines in a shared library. Second, an SE header file must be built to interface to the shared library and implement an SE class structure that returns the data in a usable form. Third, a test script is needed to display the class data and provide a starting point for any other scripts that use this data source.
Shared library rstat
client
The code was modified to make the RPC client data persistent, and it
was broken out into three routines and a debug switch, which provide
the public API to the library:
int rstat_debug; int rstat_setup(char * host); statsvar *rstat_update(); void rstat_shutdown();
If debug_switch
is set nonzero, then the debug
printf
statements are executed to produce the same output
we saw in the first rstat_client
program.
The rstat_setup
call is given the hostname to collect data
from, and returns zero if it has a problem, nonzero if it succeeds.
Each time rstat_update
is called, the RPC call is made to
the server being monitored, and an updated statsvar
data
structure is returned.
To terminate the client and free up its resources,
rstat_shutdown
is used. Failure to shutdown, when
accessing several hosts in sequence, would cause a memory leak. The new
code is shown in librstat.c
. It is compiled
with the same files as the rstat_client
program, but with
the -G
linker option to generate a shared library:
% cc -G -o librstat.so rstat_clnt.c librstat.c rstat_xdr.c
Sample code for librstat.c
:
/* * This is sample code generated by rpcgen. * These are only templates and you can use them * as a guideline for developing your own functions. */ #include "rstat.h" #include#include /* getenv, exit */ /* * Copyright (c) 1985, 1990, 1991 by Sun Microsystems, Inc. */ /* from rstat.x */ /* externally visible */ int rstat_debug = 0; /* set to 1 to get printf */ /* private */ static CLIENT *clnt; static statsvar *rs; static char * rstatproc_stats_4_arg; static char * rstatproc_havedisk_4_arg; static char hostname[80]; int rstat_setup(char *host) { clnt = clnt_create(host, RSTATPROG, RSTATVERS_VAR, "netpath"); if (clnt == (CLIENT *) NULL) { clnt_pcreateerror(host); return 0; /* fail */ } else { strncpy(hostname, host, 79); return 1; /* success */ } } statsvar *rstat_update() { u_int * result_2; int i; rs = rstatproc_stats_4((void *)&rstatproc_stats_4_arg, clnt); if (rs == (statsvar *) NULL) { clnt_perror(clnt, "call failed"); } result_2 = rstatproc_havedisk_4((void *)&rstatproc_havedisk_4_arg, clnt); if (result_2 == (u_int *) NULL) { clnt_perror(clnt, "call failed"); } if (rstat_debug) { printf("rstat information from %s at: %s", hostname, ctime(&rs->curtime.tv_sec)); printf("booted at %s", ctime(&rs->boottime.tv_sec)); printf("pgpgin: %d pgpgout: %d pswpin: %d pswpout: %d\n", rs->v_pgpgin, rs->v_pgpgout, rs->v_pswpin, rs->v_pswpout); printf("intr: %d swtch: %d avenrun: %3.1f %3.1f %3.1f\n", rs->v_intr, rs->v_swtch, rs->avenrun[0]/256.0, rs->avenrun[1]/256.0, rs->avenrun[2]/256.0); printf("ipackets: %d ierrors: %d opackets: %d oerrors: %d collisions: %d\n", rs->if_ipackets, rs->if_ierrors, rs->if_opackets, rs->if_oerrors, rs->if_collisions); /* CPU states are 0=USER 1=WAIT 2=KERNEL 3=IDLE - wierd... */ printf("usr: %d sys: %d wio: %d idle: %d\n", rs->cp_time.cp_time_val[0], rs->cp_time.cp_time_val[2], rs->cp_time.cp_time_val[1], rs->cp_time.cp_time_val[3]); for(i=0; i < rs->dk_xfer.dk_xfer_len; i++) { printf("disk: %d xfers: %d\n", i, rs->dk_xfer.dk_xfer_val[i]); } } return rs; } void rstat_shutdown() { clnt_destroy(clnt); }
SE interface rstat_class.se
SE uses a simple syntax to attach to shared libraries. The attach
statement specifies where to find the library. By default for SPARC
systems libraries should be in /opt/RICHPse/lib/sparc
. Like
many other SE classes I decided to return the measurements as
differences over time, rather than absolute values of counters. The SE
code must call rstat_update
, save the resulting data, then
on the next call, return the differences or average per-second rates
over that interval. The first time around, the average since the system
was booted can be returned instead. Time stamps are provided in the
rstat
data, so measurements can be accurate, without
needing to worry about any network delays.
The code is a little tricky where pointers are used. SE handles
pointers using integers and two routines, struct_fill
and
struct_empty
, which fill or empty an SE structure from or
to an address. The returned data from the rstat_update
call is a pointer, and the statsvar
structure also
contains two pointers -- one to the CPU data and one to the disk data.
The CPU data is always four words long on Solaris 2, so I cheated and
defined a structure rather than an array for convenience. The disk data
is variable length, so I have to read it one element at a time into a
dummy structure definition with some pointer arithmetic. I also need to
allocate an array to hold the disk data. I make the array at least big
enough to hold the number of disks on the local system, then added 100
to allow for monitoring a big system remotely from a small desktop.
It's trivial to edit this if you want more and doesn't use up enough
space to worry about on small systems.
The CPU data is returned in units of ticks. This accumulates over all CPUs, so if it adds up to several times the tick rate, there are several CPUs. It's a bit unreliable, as the math doesn't work if the system has been put through save/resume cycles or had its date changed significantly; but I can calculate how many CPUs there are.
Sample code for rstat_class.se
:
/* rstat_class.se reads data using rstat RPC protocol */ /* Adrian 23 Jan 98 */ /* * Scale factor for scaled integers used to count load averages. */ #ifndef FSCALE #define FSHIFT 8 /* bits to right of fixed binary point */ #define FSCALE (1<<FSHIFT) #endif /* ndef FSCALE */ /* this is a hack, but its not a lot of space so be generous */ #define RSTAT_DISK_COUNT(MAX_DISK+100) /* lots, 100 remote */ #define RSTAT_CPUSTATES 4 #define RSTAT_CPU_USER 0 #define RSTAT_CPU_WAIT 1 #define RSTAT_CPU_SYS 2 #define RSTAT_CPU_IDLE 3 /* this is what comes back from the rpc call */ struct statsvar { ulong cp_time_len; /* always 4 */ ulong cp_time_val_ptr; ulong dk_xfer_len; ulong dk_xfer_val_ptr; ulong v_pgpgin; ulong v_pgpgout; ulong v_pswpin; ulong v_pswpout; ulong v_intr; int if_ipackets; int if_ierrors; int if_opackets; int if_oerrors; int if_collisions; ulong v_swtch; long avenrun[3]; timeval_t boottime; timeval_t curtime; }; struct statscpu { ulong user; ulong wait; ulong system; ulong idle; }; struct statsdisk { ulong xfer; /* needed so struct fill can be used */ }; attach "librstat.so" { extern int rstat_debug; int rstat_setup(string host); ulong rstat_update(); /* returns address of statvar */ int rstat_shutdown(); }; /* strcmp handling nil strings */ int strdiff(string s1, string s2) { if (s1 == nil || s2 == nil) { return 1; /* both nil taken as being different */ } return strcmp(s1, s2); /* returns 0 if they are the same */ } class rstat_class { string hostname$; /* hostname, set it to localhost for local data */ double interval; /* seconds */ double user; /* percentages over the interval */ double system; double wait; double idle; ulong ncpus; /* number of CPUs */ ulong dk_xfer_len; /* number of disks */ ulong dk_xfer_val[RSTAT_DISK_COUNT]; /* number of ops in interval */ ulong v_pgpgin; /* number in interval, divide to get rate */ ulong v_pgpgout; ulong v_pswpin; ulong v_pswpout; ulong v_intr; int if_ipackets; int if_ierrors; int if_opackets; int if_oerrors; int if_collisions; ulong v_swtch; double avenrun_1m; double avenrun_5m; double avenrun_15m; timeval_t boottime; timeval_t curtime; rstat$() { ulong dk_old[RSTAT_DISK_COUNT]; long hz = sysconf(_SC_CLK_TCK); statsdisk dk_new; string client; statsvar sv_new; statsvar sv_old; #define DIFF(x) x = sv_new.x - sv_old.x statscpu cp_new; statscpu cp_old; int tmp; int i; if (strdiff(client, hostname$) != 0) { /* different or nil */ if (client != nil) { rstat_shutdown(); /* clear out old client */ } if (hostname$ == nil) { return; } if (rstat_setup(hostname$) == 0) { perror("bad rstat hostname"); return; } client = hostname$; } tmp = rstat_update(); if (tmp == 0) { perror("rstat read error"); client = nil; /* reset for next try */ rstat_shutdown(); return; } struct_fill(sv_new, tmp); struct_fill(cp_new, sv_new.cp_time_val_ptr); for(i = 0; i < RSTAT_DISK_COUNT && i < sv_new.dk_xfer_len; i++) { /* simulate pointer arithmetic */ struct_fill(dk_new, sv_new.dk_xfer_val_ptr + 4*i); dk_xfer_val[i] = dk_new.xfer - dk_old[i]; dk_old[i] = dk_new.xfer; } dk_xfer_len = sv_new.dk_xfer_len; if (sv_old.curtime.tv_sec == 0) { /* first time in */ sv_old.curtime = sv_new.boottime; } interval = (sv_new.curtime.tv_sec + sv_new.curtime.tv_usec/1000000.0) - (sv_old.curtime.tv_sec + sv_old.curtime.tv_usec/1000000.0); if (interval == 0.0) { return; } /* units are clock ticks */ user = (cp_new.user - cp_old.user) / interval; system = (cp_new.system - cp_old.system) / interval; wait = (cp_new.wait - cp_old.wait) / interval; idle = (cp_new.idle - cp_old.idle) / interval; /* adds up to hz per CPU but allow for rounding and trunc */ ncpus = (user + system + wait + idle) / (hz-1); if (ncpus == 0) { ncpus = 1; /* save/resume or date set problem */ } user /= ncpus; system /= ncpus; wait /= ncpus; idle /= ncpus; DIFF(v_pgpgin); DIFF(v_pgpgout); DIFF(v_pswpin); DIFF(v_pswpout); DIFF(v_intr); DIFF(if_ipackets); DIFF(if_ierrors); DIFF(if_opackets); DIFF(if_oerrors); DIFF(if_collisions); DIFF(v_swtch); avenrun_1m = sv_new.avenrun[0]/256.0; avenrun_5m = sv_new.avenrun[1]/256.0; avenrun_15m = sv_new.avenrun[2]/256.0; boottime = sv_new.boottime; curtime = sv_new.curtime; sv_old = sv_new; cp_old = cp_new; } };
SE remote rvmstat.se
example program
I decided to build something that looked a bit like vmstat
-- it has different data available and only has room to list the first
four disks, but it's a good starting place. The
rstat_class.se
code must be placed in
/opt/RICHPse/include
so it can be included by the
preprocessor. As usual the SE code to use a class is trivial. After
handling the command line arguments it is a simple printf
in a loop with a call to read the rstat_class
data and a
sleep.
SE test program rvmstat.se
:
/* test program for rstat class */ #include#include #include #include #include #include main(int argc, string argv[3]) { rstat_class rstat$r; rstat_class tmpr; int sleeptime = 5; string host = "localhost"; int lines; int start; switch(argc) { case 4: rstat_debug = 1; case 3: sleeptime = atoi(argv[2]); case 2: host = argv[1]; case 1: break; default: printf("usage: se %s [hostname] [interval] [debug]\n", argv[0]); } rstat$r.hostname$ = host; tmpr = rstat$r; /* grab initial stapshot */ start = tmpr.curtime.tv_sec; printf("Hostname %s, %d CPUs, %d disks, interval %ds, %s", host, tmpr.ncpus, tmpr.dk_xfer_len, sleeptime, ctime(&start)); for(lines=0;;lines++) { sleep(sleeptime); if (lines % 24 == 0) { printf(" load average pages disk ops faults net packets cpu\n"); printf(" 1m 5m 15m pi po 0 1 2 3 in cs in out er cl usr sys idl\n"); } tmpr = rstat$r; printf("%4.1f %3.1f %3.1f ", tmpr.avenrun_1m, tmpr.avenrun_5m, tmpr.avenrun_15m); printf("%4.0f %4.0f %3.0f %3.0f %3.0f %3.0f ", tmpr.v_pgpgin/tmpr.interval, tmpr.v_pgpgout/tmpr.interval, tmpr.dk_xfer_val[0]/tmpr.interval, tmpr.dk_xfer_val[1]/tmpr.interval, tmpr.dk_xfer_val[2]/tmpr.interval, tmpr.dk_xfer_val[3]/tmpr.interval); printf("%4.0f %5.0f %4.0f %4.0f %2.0f %2.0f ", tmpr.v_intr/tmpr.interval, tmpr.v_swtch/tmpr.interval, tmpr.if_ipackets/tmpr.interval, tmpr.if_opackets/tmpr.interval, (tmpr.if_ierrors+tmpr.if_oerrors)/tmpr.interval, tmpr.if_collisions/tmpr.interval); printf("%3.0f %3.0f %3.0f\n", tmpr.user, tmpr.system, tmpr.idle + tmpr.wait); } }
Here are some examples of use of rvmstat.se
in action. The
command takes up to three arguments, a hostname, a time interval in
seconds, and a debug flag. It defaults to localhost with a 5 second
interval. The data shown is the load averages, pages in and out (we
cannot tell the page size remotely), disk operations per-second for the
first four disks, interrupt and context switch rates, total network
packet rates, input, output error and collision packets per-second, and
the CPU user system and idle time. It's a lot more useful information
than perfmeter
displays.
% se rvmstat.se Hostname localhost, 2 CPUs, 4 disks, interval 5s, Fri Jan 23 10:27:23 1998 load average pages disk ops faults net packets cpu 1m 5m 15m pi po 0 1 2 3 in cs in out er cl usr sys idl 0.0 0.0 0.1 0 0 0 0 0 0 105 59 2 2 0 0 1 0 99 0.0 0.0 0.1 0 0 0 5 1 0 130 72 2 2 0 0 3 0 96 0.0 0.0 0.1 0 0 0 0 0 0 102 75 2 2 0 0 0 0 100
This example shows the data for another system on the same network:
% se rvmstat.se otherhost Hostname otherhost, 1 CPUs, 1 disks, interval 5s, Fri Jan 23 10:41:07 1998 load average pages disk ops faults net packets cpu 1m 5m 15m pi po 0 1 2 3 in cs in out er cl usr sys idl 0.0 0.0 0.0 0 0 0 0 0 0 31 27 0 1 0 0 0 0 100 0.0 0.0 0.0 0 0 0 0 0 0 74 46 0 1 0 0 6 1 93 0.0 0.0 0.0 0 0 4 0 0 0 288 160 0 1 0 0 29 7 63 0.0 0.0 0.0 0 0 0 0 0 0 162 87 0 1 0 0 10 2 88
If the time interval is specified, the debug flag can also be added, and the raw data counts being returned can be seen. I made the output data bold in the following example, so it can be seen amongst all the debug output.
% se rvmstat.se otherhost 2 1 rstat information from otherhost at: Fri Jan 23 10:42:50 1998 booted at Fri Jan 9 12:29:05 1998 pgpgin: 48861 pgpgout: 78299 pswpin: 0 pswpout: 0 intr: -75704389 swtch: 16122403 avenrun: 0.0 0.0 0.0 ipackets: 51700 ierrors: 0 opackets: 120217 oerrors: 6600 collisions: 0 usr: 1209483 sys: 320960 wio: 79774 idle: 19626811 disk: 0 xfers: 108022 Hostname otherhost, 1 CPUs, 1 disks, interval 2s, Fri Jan 23 10:42:50 1998 load average pages disk ops faults net packets cpu 1m 5m 15m pi po 0 1 2 3 in cs in out er cl usr sys idl rstat information from otherhost at: Fri Jan 23 10:42:52 1998 booted at Fri Jan 9 12:29:05 1998 pgpgin: 48861 pgpgout: 78299 pswpin: 0 pswpout: 0 intr: -75704298 swtch: 16122485 avenrun: 0.0 0.0 0.0 ipackets: 51703 ierrors: 0 opackets: 120221 oerrors: 6600 collisions: 0 usr: 1209483 sys: 320961 wio: 79774 idle: 19627010 disk: 0 xfers: 108022 0.0 0.0 0.0 0 0 0 0 0 0 45 41 1 2 0 0 0 0 99 rstat information from otherhost at: Fri Jan 23 10:42:54 1998 booted at Fri Jan 9 12:29:05 1998 pgpgin: 48861 pgpgout: 78299 pswpin: 0 pswpout: 0 intr: -75704249 swtch: 16122529 avenrun: 0.0 0.0 0.0 ipackets: 51705 ierrors: 0 opackets: 120223 oerrors: 6600 collisions: 0 usr: 1209483 sys: 320962 wio: 79774 idle: 19627209 disk: 0 xfers: 108022 0.0 0.0 0.0 0 0 0 0 0 0 25 22 1 1 0 0 0 1 100
Wrap up
I didn't set out to write rvmstat.se
this month, but when
I found out how easy it was to get at the rstat
information I couldn't resist seeing if I could make it callable from
SE. This code will be added to a future version of the SE toolkit and
can be downloaded as a tar file from the main SE3 download page. I hope
you find it useful, and that it may inspire some other developments that use
C code libraries to do the hard work.
|
Resources
About the author
Adrian Cockcroft joined Sun Microsystems in 1988,
and currently works as a performance specialist for the Server Division
of SMCC. He wrote
Sun Performance and Tuning: SPARC and Solaris,
published by SunSoft Press
PTR Prentice Hall.
Reach at adrian.cockcroft@sunworld.com.
If you have technical problems with this magazine, contact webmaster@sunworld.com
URL: http://www.sunworld.com/swol-02-1998/swol-02-perf.html
Last modified: