Union Types in DTrace

Unions are another kind of composite type supported by ANSI-C and D, and are closely related to structs. A union is a composite type where a set of members of different types are defined and the member objects all occupy the same region of storage. A union is therefore an object of variant type, where only one member is valid at any given time, depending on how the union has been assigned. Typically, some other variable or piece of state is used to indicate which union member is currently valid. The size of a union is the size of its largest member, and the memory alignment used for the union is the maximum alignment required by the union members.

The Oracle Solaris kstat framework defines a struct containing a union that is used in the following example to illustrate and observe C and D unions. The kstat framework is used to export a set of named counters representing kernel statistics such as memory usage and I/O throughput. The framework is used to implement utilities such as mpstat and iostat. This framework uses struct kstat_named to represent a named counter and its value and is defined as follows:

struct kstat_named {
        char name[KSTAT_STRLEN]; /* name of counter */
        uchar_t data_type;      /* data type */
        union {
                char c[16];
                int32_t i32;
                uint32_t ui32;
                long l;
                ulong_t ul;
                ...
        } value;        /* value of counter */
};

The examined declaration is shortened for illustrative purposes. The complete structure definition can be found in the <sys/kstat.h> header file and is described in kstat_named man page. The preceding declaration is valid in both ANSI-C and D, and defines a struct containing as one of its members a union value with members of various types, depending on the type of the counter. Notice that since the union itself is declared inside of another type, struct kstat_named, a formal name for the union type is omitted. This declaration style is known as an anonymous union. The member named value is of a union type described by the preceding declaration, but this union type itself has no name because it does not need to be used anywhere else. The struct member data_type is assigned a value that indicates which union member is valid for each object of type struct kstat2_named. A set of C preprocessor tokens are defined for the values of data_type. For example, the token KSTAT_DATA_CHAR is equal to zero and indicates that the member value.c is where the value is currently stored. For more information, see the kstat2_named(9S) man page.

The kstat counters can be sampled from a user process using the kstat_data_lookup() function, which returns a pointer to a struct kstat_named. For more information, see the kstat_lookup(3KSTAT) man page. The mpstat utility calls this function repeatedly as it executes in order to sample the latest counter values. Go to your shell and try running mpstat 1 and observe the output. Press Control-C in your shell to abort mpstat after a few seconds. To observe counter sampling, enable a probe that fires each time the mpstat command calls the kstat_data_lookup() function in libkstat. To do so, make use of a new DTrace provider: pid. The pid provider enables you to dynamically create probes in user processes at C symbol locations such as function entry points. You can ask the pid provider to create a probe at a user function entry and return sites by writing probe descriptions of the form:

pidprocess-ID:object-name:function-name:entry
pidprocess-ID:object-name:function-name:return

For example, if you wanted to create a probe in process ID 12345 that fires on entry to kstat_data_lookup, you would write the following probe description:

pid12345:libkstat:kstat_data_lookup:entry

The pid provider inserts dynamic instrumentation into the specified user process at the program location corresponding to the probe description. The probe implementation forces each user thread that reaches the instrumented program location to trap into the operating system kernel and enter DTrace, firing the corresponding probe. So although the instrumentation location is associated with a user process, the DTrace predicates and actions you specify still execute in the context of the operating system kernel. The pid provider is described in further detail in pid Provider.

To apply your D program to different processes, use macro variables. Macro variables are evaluated at compile time and are replaced with additional dtrace command-line arguments. Macro variables are specified using a dollar sign $ followed by an identifier or digit. If you execute the command dtrace -s script foo bar baz, the D compiler will automatically define the macro variables $1, $2, and $3 to be the tokens foo, bar, and baz respectively. You can use macro variables in D program expressions or in probe descriptions.

For more information about macro variables and reusable scripts, see Scripting in DTrace. Now that you know how to instrument user processes using their process ID, return to sampling unions.

Example 2-10 Tracing Calls to kstat_data_lookup

Type the following source code in a text editor and save it as kstat.d:

pid$1:libkstat:kstat_data_lookup:entry
{
        self->ksname = arg1;
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
        this->ksp = (kstat_named_t *) copyin(arg1, sizeof (kstat_named_t));
        printf("%s has ui64 value %u\n",
            copyinstr(self->ksname), this->ksp->value.ui64);
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
        self->ksname = NULL;
}

Now go to one of your shells and execute the command zonestat to start zonestat running in a mode where it samples statistics and reports them once per second. Once zonestat is running, execute the command dtrace -q -s kstat.d `pgrep zonestatd` in your other shell. You will see output corresponding to the statistics that are being accessed. Press Control-C to abort dtrace and return to the shell prompt.

# dtrace -q -s kstat.d `pgrep 

zonestatd`
hat_fault has ui64 value 0
as_fault has ui64 value 48053
maj_fault has ui64 value 1144
xcalls has ui64 value 123832170
intr has ui64 value 165264090
intrthread has ui64 value 124094974
pswitch has ui64 value 840625
inv_swtch has ui64 value 1484
cpumigrate has ui64 value 36284
mutex_adenters has ui64 value 35574
rw_rdfails has ui64 value 2
rw_wrfails has ui64 value 2
...
^C
#

If you capture the output in each terminal window and subtract each value from the value reported by the previous iteration through the statistics, you should be able to correlate the dtrace output with the mpstat output. The example program records the counter name pointer on entry to the lookup function, and then performs most of the tracing work on return from kstat_data_lookup. The D built-in functions copyinstr() and copyin() copy the function results from the user process back into DTrace when arg1 (the return value) is not NULL. Once the kstat data has been copied, the example reports the ui64 counter value from the union. This simplified example assumes that mpstat samples counters that use the value.ui64 member. As an exercise, try recoding kstat.d to use multiple predicates and print out the union member corresponding to the data_type member. You can also try to create a version of kstat.d that computes the difference between successive data values and actually produces output similar to mpstat.