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 16, 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:
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 t 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; }