Skip Navigation Links | |
Exit Print View | |
Oracle Solaris 11.1 Dynamic Tracing Guide Oracle Solaris 11.1 Information Library |
Probe Clauses and Declarations
Compilation and Instrumentation
Variables and Arithmetic Expressions
Types, Operators, and Expressions
Increment and Decrement Operators
Array Declarations and Storage
Pointer and Array Relationship
13. Statically Defined Tracing for User Applications
D provides two basic types of variables for use in your tracing programs: scalar variables and associative arrays. This chapter explores the rules for D variables in more detail and how variables can be associated with different scopes. A special kind of array variable, called an aggregation, is discussed in Chapter 3, Aggregations.
Note -
Scalar variables and associative arrays have a global scope and are not multi-processor safe (MP-safe). It means that the value of these variables can be changed by more than one processor and thus there are chances that the variable can became corrupt.
Aggregations are MP-safe even though they have a global scope.
Scalar variables are used to represent individual fixed-size data objects, such as integers and pointers. Scalar variables can also be used for fixed-size objects that are composed of one or more primitive or composite types. D provides the ability to create both arrays of objects as well as composite structures. DTrace also represents strings as fixed-size scalars by permitting them to grow up to a predefined maximum length. Control over string length in your D program is discussed further in Strings.
Scalar variables are created automatically the first time you assign a value to a previously undefined identifier in your D program. For example, to create a scalar variable named x of type int, you can simply assign it a value of type int in any probe clause:
BEGIN { x = 123; }
Scalar variables created in this manner are global variables: their name and data storage location is defined once and is visible in every clause of your D program. Any time you reference the identifier x, you are referring to a single storage location associated with this variable.
Unlike ANSI-C, D does not require explicit variable declarations. If you do want to declare a global variable to assign its name and type explicitly before using it, you can place a declaration outside of the probe clauses in your program as shown in the following example. Explicit variable declarations are not necessary in most D programs, but are sometimes useful when you want to carefully control your variable types or when you want to begin your program with a set of declarations and comments documenting your program's variables and their meanings.
int x; /* declare an integer x for later use */ BEGIN { x = 123; ... }
Unlike ANSI-C declarations, D variable declarations may not assign initial values. You must use a BEGIN probe clause to assign any initial values. All global variable storage is filled with zeroes by DTrace before you first reference the variable.
The D language definition places no limit on the size and number of D variables, but limits are defined by the DTrace implementation and by the memory available on your system. The D compiler will enforce any of the limitations that can be applied at the time you compile your program. You can learn more about how to tune options related to program limits in Chapter 10, Options and Tunables.
Associative arrays are used to represent collections of data elements that can be retrieved by specifying a name called a key. D associative array keys are formed by a list of scalar expression values called a tuple. You can think of the array tuple itself as an imaginary parameter list to a function that is called to retrieve the corresponding array value when you reference the array. Each D associative array has a fixed key signature consisting of a fixed number of tuple elements where each element has a given, fixed type. You can define different key signatures for each array in your D program.
Associative arrays differ from normal, fixed-size arrays in that they have no predefined limit on the number of elements, the elements can be indexed by any tuple as opposed to just using integers as keys, and the elements are not stored in preallocated consecutive storage locations. Associative arrays are useful in situations where you would use a hash table or other simple dictionary data structure in a C, C++, or Java language program. Associative arrays give you the ability to create a dynamic history of events and state captured in your D program that you can use to create more complex control flows.
To define an associative array, you write an assignment expression of the form:
name [ key ] = expression ;
where name is any valid D identifier and key is a comma-separated list of one or more expressions. For example, the following statement defines an associative array a with key signature [ int, string ] and stores the integer value 456 in a location named by the tuple [ 123, "hello" ]:
a[123, "hello"] = 456;
The type of each object contained in the array is also fixed for all elements in a given array. Because a was first assigned using the integer 456, every subsequent value stored in the array will also be of type int. You can use any of the assignment operators defined in Types, Operators, and Expressions to modify associative array elements, subject to the operand rules defined for each operator. The D compiler will produce an appropriate error message if you attempt an incompatible assignment. You can use any type with an associative array key or value that you can use with a scalar variable. You cannot nest an associative array within another associative array as a key or value.
You can reference an associative array using any tuple that is compatible with the array key signature. The rules for tuple compatibility are similar to those for function calls and variable assignments: the tuple must be of the same length and each type in the list of actual parameters must be compatible with the corresponding type in the formal key signature. For example, if an associative array x is defined as follows:
x[123ull] = 0;
then the key signature is of type unsigned long long and the values are of type int. This array can also be referenced using the expression x['a'] because the tuple consisting of the character constant 'a' of type int and length one is compatible with the key signature unsigned long long according to the arithmetic conversion rules described in Type Conversions.
If you need to explicitly declare a D associative array before using it, you can create a declaration of the array name and key signature outside of the probe clauses in your program source code:
int x[unsigned long long, char]; BEGIN { x[123ull, 'a'] = 456; }
Once an associative array is defined, references to any tuple of a compatible key signature are permitted, even if the tuple in question has not been previously assigned. Accessing an unassigned associative array element is defined to return a zero-filled object. A consequence of this definition is that underlying storage is not allocated for an associative array element until a non-zero value is assigned to that element. Conversely, assigning an associative array element to zero causes DTrace to deallocate the underlying storage. This behavior is important because the dynamic variable space out of which associative array elements are allocated is finite; if it is exhausted when an allocation is attempted, the allocation will fail and an error message will be generated indicating a dynamic variable drop. Always assign zero to associative array elements that are no longer in use. See Chapter 10, Options and Tunables for other techniques to eliminate dynamic variable drops.
DTrace provides the ability to declare variable storage that is local to each operating system thread, as opposed to the global variables demonstrated earlier in this chapter. Thread-local variables are useful in situations where you want to enable a probe and mark every thread that fires the probe with some tag or other data. Creating a program to solve this problem is easy in D because thread-local variables share a common name in your D code but refer to separate data storage associated with each thread. Thread-local variables are referenced by applying the -> operator to the special identifier self:
syscall::read:entry { self->read = 1; }
This D fragment example enables the probe on the read(2) system call and associates a thread-local variable named read with each thread that fires the probe. Similar to global variables, thread-local variables are created automatically on their first assignment and assume the type used on the right-hand side of the first assignment statement (in this example, int).
Each time the variable self->read is referenced in your D program, the data object referenced is the one associated with the operating system thread that was executing when the corresponding DTrace probe fired. You can think of a thread-local variable as an associative array that is implicitly indexed by a tuple that describes the thread's identity in the system. A thread's identity is unique over the lifetime of the system: if the thread exits and the same operating system data structure is used to create a new thread, this thread does not reuse the same DTrace thread-local storage identity.
Once you have defined a thread-local variable, you can reference it for any thread in the system even if the variable in question has not been previously assigned for that particular thread. If a thread's copy of the thread-local variable has not yet been assigned, the data storage for the copy is defined to be filled with zeroes. As with associative array elements, underlying storage is not allocated for a thread-local variable until a non-zero value is assigned to it. Also as with associative array elements, assigning zero to a thread-local variable causes DTrace to deallocate the underlying storage. Always assign zero to thread-local variables that are no longer in use. See Chapter 10, Options and Tunables for other techniques to fine-tune the dynamic variable space from which thread-local variables are allocated.
Thread-local variables of any type can be defined in your D program, including associative arrays. Some example thread-local variable definitions are:
self->x = 123; /* integer value */ self->s = "hello"; /* string value */ self->a[123, 'a'] = 456; /* associative array */
Like any D variable, you don't need to explicitly declare thread-local variables before using them. If you want to create a declaration anyway, you can place one outside of your program clauses by prepending the keyword self:
self int x; /* declare int x as a thread-local variable */ syscall::read:entry { self->x = 123; }
Thread-local variables are kept in a separate namespace from global variables so you can reuse names. Remember that x and self->x are not the same variable if you overload names in your program! The following example shows how to use thread-local variables. In a text editor, type in the following program and save it in a file named rtime.d:
Example 2-3 rtime.d: Compute Time Spent in read(2)
syscall::read:entry { self->t = timestamp; } syscall::read:return /self->t != 0/ { printf("%d/%d spent %d nsecs in read(2)\n", pid, tid, timestamp - self->t); /* * We're done with this thread-local variable; assign zero to it to * allow the DTrace runtime to reclaim the underlying storage. */ self->t = 0; }
Now go to your shell and start the program running. Wait a few seconds and you should start to see some output. If no output appears, try running a few commands.
# dtrace -q -s rtime.d 100480/1 spent 11898 nsecs in read(2) 100441/1 spent 6742 nsecs in read(2) 100480/1 spent 4619 nsecs in read(2) 100452/1 spent 19560 nsecs in read(2) 100452/1 spent 3648 nsecs in read(2) 100441/1 spent 6645 nsecs in read(2) 100452/1 spent 5168 nsecs in read(2) 100452/1 spent 20329 nsecs in read(2) 100452/1 spent 3596 nsecs in read(2) ... ^C #
rtime.d uses a thread-local variable named to capture a timestamp on entry to read(2) by any thread. Then, in the return clause, the program prints out the amount of time spent in read(2) by subtracting self->t from the current timestamp. The built-in D variables pid and tid report the process ID and thread ID of the thread performing the read(2). Because self->t is no longer needed once this information is reported, it is then assigned 0 to allow DTrace to reuse the underlying storage associated with t for the current thread.
Typically you will see many lines of output without even doing anything because, behind the scenes, server processes and daemons are executing read(2) all the time even when you aren't doing anything. Try changing the second clause of rtime.d to use the execname variable to print out the name of the process performing a read(2) to learn more:
printf("%s/%d spent %d nsecs in read(2)\n", execname, tid, timestamp - self->t);
If you find a process that's of particular interest, add a predicate to learn more about its read(2) behavior:
syscall::read:entry /execname == "Xsun"/ { self->t = timestamp; }
You can also define D variables whose storage is reused for each D program clause. Clause-local variables are similar to automatic variables in a C, C++, or Java language program that are active during each invocation of a function. Like all D program variables, clause-local variables are created on their first assignment. These variables can be referenced and assigned by applying the -> operator to the special identifier this:
BEGIN { this->secs = timestamp / 1000000000; ... }
If you want to explicitly declare a clause-local variable before using it, you can do so using the this keyword:
this int x; /* an integer clause-local variable */ this char c; /* a character clause-local variable */ BEGIN { this->x = 123; this->c = 'D'; }
Clause-local variables are only active for the lifetime of a given probe clause. After DTrace performs the actions associated with your clauses for a given probe, the storage for all clause-local variables is reclaimed and reused for the next clause. For this reason, clause-local variables are the only D variables that are not initially filled with zeroes. Note that if your program contains multiple clauses for a single probe, any clause-local variables will remain intact as the clauses are executed, as shown in the following example:
Example 2-4 clause.d: Clause-local Variables
int me; /* an integer global variable */ this int foo; /* an integer clause-local variable */ tick-1sec { /* * Set foo to be 10 if and only if this is the first clause executed. */ this->foo = (me % 3 == 0) ? 10 : this->foo; printf("Clause 1 is number %d; foo is %d\n", me++ % 3, this->foo++); } tick-1sec { /* * Set foo to be 20 if and only if this is the first clause executed. */ this->foo = (me % 3 == 0) ? 20 : this->foo; printf("Clause 2 is number %d; foo is %d\n", me++ % 3, this->foo++); } tick-1sec { /* * Set foo to be 30 if and only if this is the first clause executed. */ this->foo = (me % 3 == 0) ? 30 : this->foo; printf("Clause 3 is number %d; foo is %d\n", me++ % 3, this->foo++); }
Because the clauses are always executed in program order, and because clause-local variables are persistent across different clauses enabling the same probe, running the above program will always produce the same output:
# dtrace -q -s clause.d Clause 1 is number 0; foo is 10 Clause 2 is number 1; foo is 11 Clause 3 is number 2; foo is 12 Clause 1 is number 0; foo is 10 Clause 2 is number 1; foo is 11 Clause 3 is number 2; foo is 12 Clause 1 is number 0; foo is 10 Clause 2 is number 1; foo is 11 Clause 3 is number 2; foo is 12 Clause 1 is number 0; foo is 10 Clause 2 is number 1; foo is 11 Clause 3 is number 2; foo is 12 ^C
While clause-local variables are persistent across clauses enabling the same probe, their values are undefined in the first clause executed for a given probe. Be sure to assign each clause-local variable an appropriate value before using it, or your program may have unexpected results.
Clause-local variables can be defined using any scalar variable type, but associative arrays may not be defined using clause-local scope. The scope of clause-local variables only applies to the corresponding variable data, not to the name and type identity defined for the variable. Once a clause-local variable is defined, this name and type signature may be used in any subsequent D program clause. You cannot rely on the storage location to be the same across different clauses.
You can use clause-local variables to accumulate intermediate results of calculations or as temporary copies of other variables. Access to a clause-local variable is much faster than access to an associative array. Therefore, if you need to reference an associative array value multiple times in the same D program clause, it is more efficient to copy it into a clause-local variable first and then reference the local variable repeatedly.
The following table provides a complete list of D built-in variables. All of these variables are scalar global variables; no thread-local or clause-local variables or built-in associative arrays are currently defined by D.
Table 2-13 DTrace Built-in Variables
|
Functions built into the D language such as trace are discussed in Chapter 4, Actions and Subroutines.
D uses the backquote character (`) as a special scoping operator for accessing variables that are defined in the operating system and not in your D program. For example, the Solaris kernel contains a C declaration of a system tunable named kmem_flags for enabling memory allocator debugging features. See the Oracle Solaris 11.1 Tunable Parameters Reference Manual for more information about kmem_flags. This tunable is declared as a C variable in the kernel source code as follows:
int kmem_flags;
To access the value of this variable in a D program, use the D notation:
`kmem_flags
DTrace associates each kernel symbol with the type used for the symbol in the corresponding operating system C code, providing easy source-based access to the native operating system data structures. In order to use external operating system variables, you will need access to the corresponding operating system source code.
When you access external variables from a D program, you are accessing the internal implementation details of another program such as the operating system kernel or its device drivers. These implementation details do not form a stable interface upon which you can rely! Any D programs you write that depend on these details might cease to work when you next upgrade the corresponding piece of software. For this reason, external variables are typically used by kernel and device driver developers and service personnel in order to debug performance or functionality problems using DTrace. To learn more about the stability of your D programs, refer to Chapter 18, Stability.
Kernel symbol names are kept in a separate namespace from D variable and function identifiers, so you never need to worry about these names conflicting with your D variables. When you prefix a variable with a backquote, the D compiler searches the known kernel symbols in order using the list of loaded modules in order to find a matching variable definition. Because the Solaris kernel supports dynamically loaded modules with separate symbol namespaces, the same variable name might be used more than once in the active operating system kernel. You can resolve these name conflicts by specifying the name of the kernel module whose variable should be accessed prior to the backquote in the symbol name. For example, each loadable kernel module typically provides a _fini(9E) function, so to refer to the address of the _fini function provided by a kernel module named foo, you would write:
foo`_fini
You can apply any of the D operators to external variables, except those that modify values, subject to the usual rules for operand types. When you launch DTrace, the D compiler loads the set of variable names corresponding to the active kernel modules, so declarations of these variables are not required. You may not apply any operator to an external variable that modifies its value, such as = or +=. For safety reasons, DTrace prevents you from damaging or corrupting the state of the software you are observing.