probe_cntl.c revision cb6207858a9fcc2feaee22e626912fba281ac969
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2007 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#pragma ident "%Z%%M% %I% %E% SMI"
/*
* Includes
*/
#ifndef DEBUG
#define NDEBUG 1
#endif
#include <thread.h>
#include <pthread.h>
#include <sys/lwp.h>
#include <synch.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#ifdef sparc
#include <setjmp.h>
#endif /* sparc */
#include "tnf_trace.h"
/*
* Typedefs
*/
typedef tnf_ops_t *(*tnf_context_t)(void);
typedef void * (*start_func_t)(void *arg);
typedef int (*tnf_thr_create_func_t)(void *stk,
size_t stksize,
start_func_t startfunc,
void *arg,
long flags,
thread_t *newthread);
typedef int (*tnf_pthread_create_func_t)(pthread_t *thr,
const pthread_attr_t *attr,
start_func_t startfunc,
void * arg);
typedef void (*tnf_thr_exit_func_t)(void *) __NORETURN;
typedef void (*tnf_pthread_exit_func_t)(void *) __NORETURN;
typedef pid_t (*fork_t)(void);
typedef int (*tnf_thr_stksegment_func_t)(stack_t *s);
typedef struct args {
start_func_t real_func;
void *real_arg;
} args_t;
/*
* Local Declarations
*/
static void * tnf_threaded_test(void *dummy,
tnf_probe_control_t *probe_p,
tnf_probe_setup_t *set_p);
static void * tnf_non_threaded_test(void *dummy,
tnf_probe_control_t *probe_p,
tnf_probe_setup_t *set_p);
static tnf_ops_t *tnf_probe_getfunc(void);
static void *probestart(void *arg);
static pid_t common_fork(fork_t real_fork);
static void probe_setup(void *data);
static tnf_ops_t *tnf_get_ops();
/*
* Static Globals
*/
extern tnf_ops_t tnf_trace_initial_tpd;
static void *tpd = &tnf_trace_initial_tpd;
#ifdef sparc
static size_t tnf_probe_dsize = 0;
#endif /* sparc */
/*
* Project Private interfaces:
* These are interfaces between prex and libtnfw, or
* between libtnfw and libtthread.
*/
/* variable indicates if libtnfw has sync'ed up with libthread or not */
long __tnf_probe_thr_sync = 0;
/* head of the list that is used to chain all probes */
tnf_probe_control_t *__tnf_probe_list_head = NULL;
int __tnf_probe_list_valid = 0;
/* notify function that libthread calls after primordial thread is created */
void __tnf_probe_notify(void);
tnf_probe_test_func_t tnf_threaded_test_addr = tnf_threaded_test;
tnf_probe_test_func_t tnf_non_threaded_test_addr = tnf_non_threaded_test;
/*
* Externs
*/
#pragma weak thr_probe_getfunc_addr
extern tnf_context_t thr_probe_getfunc_addr;
#pragma weak thr_probe_setup
extern void thr_probe_setup(void *);
/* ---------------------------------------------------------------- */
/* ----------------------- Public Functions ----------------------- */
/* ---------------------------------------------------------------- */
/*
* probe_setup() - the thread probe setup function for the non-threaded
* case.
*/
static void
probe_setup(void *data)
{
#ifdef DEBUG
/* #### - TEMPORARY */
fprintf(stderr, "probe_setup: \n");
#endif
tpd = data;
} /* end probe_setup */
/*
* __tnf_probe_notify() - libthread calls this function to notify us
* that the primordial thread has been created.
*/
void
__tnf_probe_notify(void)
{
tnf_probe_control_t *prbctl_p;
tnf_probe_test_func_t test_func;
/* paranoia: thr_probe_setup should be defined */
assert(thr_probe_setup != 0);
if (thr_probe_setup != 0) thr_probe_setup(tpd);
/*
* no race with prex if we set flag first
* - this is an idempotent operation
*/
__tnf_probe_thr_sync = 1;
#ifdef DEBUG
{
char tmp_buf[512];
(void) sprintf(tmp_buf, "__tnf_probe_notify: \n");
(void) write(2, tmp_buf, strlen(tmp_buf));
}
#endif
/*
* Use dlsym to test for the present of "thr_probe_getfunc_addr" .
*/
test_func = (((int(*)())dlsym(RTLD_DEFAULT,
"thr_probe_getfunc_addr")) != NULL) ? tnf_threaded_test : 0;
assert(test_func);
/*
* I think in this case that we do not need to check the
* __tnf_probe_list_valid flag since __tnf_probe_notify is
* called very early.
*/
/* replace all existing test functions with libthread's test func */
for (prbctl_p = __tnf_probe_list_head; prbctl_p;
prbctl_p = prbctl_p->next)
if (prbctl_p->test_func)
prbctl_p->test_func = test_func;
return;
} /* end __tnf_probe_notify */
/*
* _tnf_fork_thread_setup - function called by buffering layer
* whenever it finds a thread in the newly forked process that
* hasn't been re-initialized in this process.
*/
void
_tnf_fork_thread_setup(void)
{
tnf_ops_t *ops;
#ifdef DEBUGFUNCS
{
char tmp_buf[512];
(void) sprintf(tmp_buf, "in _tnf_fork_thread_setup: \n");
(void) write(2, tmp_buf, strlen(tmp_buf));
}
#endif
/* get the tpd */
ops = tnf_get_ops();
if (!ops)
return;
/* null out tag_index, so that a new one is initialized and written */
ops->schedule.record_p = 0;
return;
}
/* ---------------------------------------------------------------- */
/* ---------------------- Interposed Functions -------------------- */
/* ---------------------------------------------------------------- */
/*
* thr_create() - this function is interposed in front of the
* actual thread create function in libthread.
*/
int
thr_create(void *stk,
size_t stksize,
void * (*real_func)(void *),
void *real_arg,
long flags,
thread_t *new_thread)
{
static tnf_thr_create_func_t real_thr_create = NULL;
args_t *arg_p;
#ifdef VERYVERBOSE
fprintf(stderr, "hello from the interposed thr_create parent\n");
#endif
/* use dlsym to find the address of the "real" thr_create function */
if (real_thr_create == NULL) {
real_thr_create = (tnf_thr_create_func_t)
dlsym(RTLD_NEXT, "thr_create");
}
assert(real_thr_create);
/* set up the interposed argument block */
arg_p = (args_t *)malloc(sizeof (args_t));
assert(arg_p);
arg_p->real_func = real_func;
arg_p->real_arg = real_arg;
return ((*real_thr_create)(stk, stksize, probestart, (void *) arg_p,
flags, new_thread));
} /* end thr_create */
int
pthread_create(pthread_t *new_thread_id,
const pthread_attr_t *attr,
void * (*real_func)(void *),
void *real_arg)
{
static tnf_pthread_create_func_t real_pthread_create = NULL;
args_t *arg_p;
#ifdef VERYVERBOSE
fprintf(stderr, "hello from the interposed pthread_create parent\n");
#endif
/* use dlsym to find the address of the "real" pthread_create func */
if (real_pthread_create == NULL) {
real_pthread_create = (tnf_pthread_create_func_t)
dlsym(RTLD_NEXT, "pthread_create");
}
assert(real_pthread_create);
/* set up the interposed argument block */
arg_p = (args_t *)malloc(sizeof (args_t));
assert(arg_p);
arg_p->real_func = real_func;
arg_p->real_arg = real_arg;
return ((*real_pthread_create)(new_thread_id, attr, probestart,
(void *) arg_p));
} /* end pthread_create */
void
thr_exit(void * status)
{
static tnf_thr_exit_func_t real_thr_exit = NULL;
/* use dlsym to find the address of the "real" pthread_create func */
if (real_thr_exit == NULL) {
real_thr_exit = (tnf_thr_exit_func_t)
dlsym(RTLD_NEXT, "thr_exit");
}
assert(real_thr_exit);
/*
* Calling tnf_thread_disable() whenever a thread exits...
* This has the side-effect of unlocking our currently
* locked block in the trace buffer. This keeps a dying
* thread from taking a block with it when it dies, but
* it means that we won't be able to trace events from
* the thread-specific data destructors. We will lose
* out on any events a thread spits out AFTER is calls thr_exit().
* This code was added to fix a bug where tracing breaks when trying
* to trace a program with large numbers of thread-ids.
*
* Addendum:
* Now you can't get events for thr_exit using an interposition library.
* Since thr_exit is a really helpful event, this is a problem.
* Also, breaking this interposition will probably break
* BAT, the DevPro TNF perf tool.
*
* Addendum:
* Correction: You can get interposition events if the interposition
* library comes BEFORE libtnfprobe.so. But not, if the interp.
* library comes AFTER libtnfprobe.so. This is a more difficult
* constraint that it might sound like because of the following:
* The tnfctl functional interface and the prex command line
* interface provide convenience features where you can supply
* a character string argument which will be put into LD_PRELOAD
* for you. Unfortunately, this string gets appended AFTER
* libtnfprobe.so by the tnfctl library(and also hence by the
* prex -l option).
* Luckily, when libtnfprobe is added by the tnfctl library, it is
* added AFTER an existing contents of the LD_PRELOAD variable.
*
* Therefore, if you are using an interposition library to collect
* thr_exit and pthread_exit events, THEN you should NOT use 'prex -l'
* or the 'ld_preload' argument to tnfctl_exec_open(), instead, you
* should be sure to put the interposition library into the LD_PRELOAD
* variable yourself.
*
*/
tnf_thread_disable();
((*real_thr_exit)(status));
}
void
pthread_exit(void * status)
{
static tnf_pthread_exit_func_t real_pthread_exit = NULL;
/* use dlsym to find the address of the "real" pthread_create func */
if (real_pthread_exit == NULL) {
real_pthread_exit = (tnf_pthread_exit_func_t)
dlsym(RTLD_NEXT, "pthread_exit");
}
assert(real_pthread_exit);
/* see the comment in thr_exit about tnf_thread_disable() */
tnf_thread_disable();
((*real_pthread_exit)(status));
}
/*
* function to be interposed in front of _resume. We invalidate the
* schedule record in case the lwpid changes the next time this
* thread is scheduled.
*/
#pragma weak _resume_ret = _tnf_resume_ret
void
_tnf_resume_ret(void *arg1)
{
static void (*real_resume_ret)(void *) = NULL;
tnf_ops_t *ops;
if (real_resume_ret == NULL) {
real_resume_ret = (void (*)(void *)) dlsym(RTLD_NEXT,
"_resume_ret");
}
assert(real_resume_ret);
ops = tnf_get_ops();
if (ops) {
/*
* invalidate the schedule record. This forces it
* to get re-initialized with the new lwpid the next
* time this thread gets scheduled
*/
if (ops->schedule.lwpid != _lwp_self())
ops->schedule.record_p = 0;
}
real_resume_ret(arg1);
}
/*
* Functions to be interposed in front of fork and fork1.
*
* NOTE: we can't handle vfork, because the child would ruin the parent's
* data structures. We therefore don't interpose, letting the child's
* events appear as though they were the parent's. A slightly cleaner
* way to handle vfork would be to interpose on vfork separately to
* change the pid and anything else needed to show any events caused
* by the child as its events, and then interpose on the exec's as
* well to set things back to the way they should be for the parent.
* But this is a lot of work, and it makes almost no difference, since the
* child typically exec's very quickly after a vfork.
*/
#pragma weak fork = _tnf_fork
pid_t
_tnf_fork(void)
{
static fork_t real_fork = NULL;
if (real_fork == NULL) {
real_fork = (fork_t)dlsym(RTLD_NEXT, "fork");
}
assert(real_fork);
return (common_fork(real_fork));
}
#pragma weak fork1 = _tnf_fork1
pid_t
_tnf_fork1(void)
{
static fork_t real_fork = NULL;
if (real_fork == NULL) {
real_fork = (fork_t)dlsym(RTLD_NEXT, "fork1");
}
assert(real_fork);
return (common_fork(real_fork));
}
#ifdef sparc
/*
* Function to be interposed in front of thr_stksegment
* _tnf_thr_stksegment() - used to hide the probestart() allocated data
* on the thread stack, ensuring that the caller receives a pointer to the
* true bottom (ie, usable) portion of the stack, and the size thereof.
*
* NOTE: On sparc systems, failure to allow for the presense of tnf data
* on the stack would cause TNF probes to fail across doorfs calls. The
* i386 version of door_return decides to "skip over some slop", so no
* interpose function is required for x86; if the 512 byte 'slop skip'
* is ever removed from the i386 door_return, then it will also need
* interpose function intervention.
*/
#pragma weak thr_stksegment = _tnf_thr_stksegment
static int
_tnf_thr_stksegment(stack_t *s)
{
static tnf_thr_stksegment_func_t real_thr_stksegment = NULL;
int err;
#ifdef VERYVERBOSE
fprintf(stderr, "hello from the interposed thr_stksegment\n");
#endif
if (real_thr_stksegment == NULL) {
real_thr_stksegment = (tnf_thr_stksegment_func_t)
dlsym(RTLD_NEXT, "thr_stksegment");
}
assert(real_thr_stksegment);
err = ((*real_thr_stksegment)(s));
if (err == 0) {
s->ss_sp = (void *)((caddr_t)s->ss_sp - tnf_probe_dsize);
s->ss_size -= tnf_probe_dsize;
}
return (err);
}
#endif /* sparc */
/* ---------------------------------------------------------------- */
/* ----------------------- Private Functions ---------------------- */
/* ---------------------------------------------------------------- */
/*
* tnf_probe_getfunc() - default test function if libthread is not
* present
*/
static tnf_ops_t *
tnf_probe_getfunc(void)
{
/* test function to be used if libthread is not linked in */
#ifdef DEBUGFUNCS
{
char tmp_buf[512];
(void) sprintf(tmp_buf, "tnf_probe_getfunc: \n");
(void) write(2, tmp_buf, strlen(tmp_buf));
}
#endif
return (tpd);
} /* end tnf_probe_getfunc */
/*
* probestart() - this function is called as the start_func by the
* interposed thr_create() and pthread_create(). It calls the real start
* function.
*/
static void *
probestart(void * arg)
{
args_t *args_p = (args_t *)arg;
start_func_t real_func;
void *real_arg;
tnf_ops_t ops; /* allocated on stack */
void *real_retval;
#ifdef VERYVERBOSE
fprintf(stderr, "hello from the interposed thr_create child\n");
#endif
#ifdef sparc
/*
* if the size of the probe data has not yet been calculated,
* initialize a jmpbuffer and calculate the amount of stack space
* used by probestart: %fp - %sp from jmp_buf
* Not expecting anything to actually longjmp here, so that is
* handled as an error condition.
*/
if (tnf_probe_dsize == 0) {
jmp_buf tnf_jmpbuf;
if (setjmp(tnf_jmpbuf) != 0) {
(void) write(2,
"probestart: unexpected longjmp\n", 32);
assert(0);
}
tnf_probe_dsize = (size_t)(tnf_jmpbuf[3] - tnf_jmpbuf[1]);
}
#endif /* sparc */
/* initialize ops */
(void) memset(&ops, 0, sizeof (ops)); /* zero ops */
ops.mode = TNF_ALLOC_REUSABLE;
ops.alloc = tnfw_b_alloc;
ops.commit = tnfw_b_xcommit;
ops.rollback = tnfw_b_xabort;
/* copy (and free) the allocated arg block */
real_func = args_p->real_func;
real_arg = args_p->real_arg;
free(args_p);
/* paranoia: thr_probe_setup should be defined */
assert(thr_probe_setup != 0);
if (thr_probe_setup != 0) thr_probe_setup(&ops);
#ifdef VERYVERBOSE
fprintf(stderr, "in middle of interposed start procedure\n");
#endif
real_retval = (*real_func)(real_arg);
/*
* we need to write a NULL into the tpd pointer to disable
* tracing for this thread.
* CAUTION: never make this function tail recursive because
* tpd is allocated on stack.
*/
/* This should be handled by the call to tnf_thread_disable() */
/* if (thr_probe_setup != 0) */
/* thr_probe_setup(NULL); */
/* see the comment in thr_exit about tnf_thread_disable */
tnf_thread_disable();
return (real_retval);
} /* end probestart */
static thread_key_t tpd_key = THR_ONCE_KEY;
static tnf_ops_t *stashed_tpd = NULL;
/*
* tnf_thread_disable: API to disable a thread
*/
void
tnf_thread_disable(void)
{
tnf_ops_t *ops;
if (thr_probe_setup != 0) {
/* threaded client */
/* REMIND: destructor function ? */
(void) thr_keycreate_once(&tpd_key, NULL);
/* get the tpd */
ops = thr_probe_getfunc_addr();
/* check ops to ensure function is idempotent */
if (ops != NULL) {
/* unlock currently held blocks */
tnfw_b_release_block(&ops->wcb);
/* disable the thread */
thr_probe_setup(NULL);
/* stash the tpd */
(void) thr_setspecific(tpd_key, ops);
}
} else {
/* non-threaded client */
/* get the tpd */
ops = tnf_probe_getfunc();
if (ops != NULL) {
/* disable the process */
probe_setup(NULL);
/* stash the tpd */
stashed_tpd = ops;
}
}
}
/*
* tnf_thread_enable: API to enable a thread
*/
void
tnf_thread_enable(void)
{
tnf_ops_t *ops;
if (thr_probe_setup != 0) {
/* threaded client */
ops = pthread_getspecific(tpd_key);
if (ops)
thr_probe_setup(ops);
} else {
/* non-threaded client */
ops = stashed_tpd;
if (ops)
probe_setup(ops);
}
}
/*
* common_fork - code that is common among the interpositions of
* fork, fork1, and vfork
*/
static pid_t
common_fork(fork_t real_fork)
{
pid_t retval;
tnf_ops_t *ops;
tnf_tag_data_t *metatag_data;
#ifdef DEBUGFUNCS
{
char tmp_buf[512];
(void) sprintf(tmp_buf, "in interposed fork: \n");
(void) write(2, tmp_buf, strlen(tmp_buf));
}
#endif
if ((_tnfw_b_control->tnf_state == TNFW_B_NOBUFFER) &&
(tnf_trace_file_name[0] != '\0')) {
/*
* if no buffer has been allocated yet, and prex plugged in
* name...
*/
ops = tnf_get_ops();
if (ops == NULL) {
/*
* get it from stashed location
* don't enable thread though
*/
if (thr_probe_setup != 0) {
/* threaded client */
ops = pthread_getspecific(tpd_key);
} else {
/* non-threaded client */
ops = stashed_tpd;
}
}
/*
* ops shouldn't be NULL. But, if it is, then we don't
* initialize tracing. In the child, tracing will be
* set to broken.
*/
if (ops) {
/* initialize tracing */
ops->busy = 1;
metatag_data = TAG_DATA(tnf_struct_type);
metatag_data->tag_desc(ops, metatag_data);
/* commit the data */
(void) ops->commit(&(ops->wcb));
ops->busy = 0;
}
}
retval = real_fork();
if (retval == 0) {
/* child process */
_tnfw_b_control->tnf_pid = getpid();
if ((_tnfw_b_control->tnf_state == TNFW_B_NOBUFFER) &&
(tnf_trace_file_name[0] != '\0')) {
/*
* race condition, prex attached after condition was
* checked in parent, so both parent and child point at
* the same file name and will overwrite each other.
* So, we set tracing to broken in child. We could
* invent a new state called RACE and use prex to
* reset it, if needed...
*/
tnf_trace_file_name[0] = '\0';
_tnfw_b_control->tnf_state = TNFW_B_BROKEN;
} else if (_tnfw_b_control->tnf_state == TNFW_B_RUNNING) {
/* normal expected condition */
_tnfw_b_control->tnf_state = TNFW_B_FORKED;
}
}
return (retval);
}
/*
* tnf_threaded_test
*/
/*ARGSUSED0*/
static void *
tnf_threaded_test(void *dummy, tnf_probe_control_t *probe_p,
tnf_probe_setup_t *set_p)
{
tnf_ops_t *tpd_p;
tpd_p = thr_probe_getfunc_addr();
if (tpd_p) {
return (probe_p->alloc_func(tpd_p, probe_p, set_p));
}
return (NULL);
}
/*
* tnf_non_threaded_test
*/
/*ARGSUSED0*/
static void *
tnf_non_threaded_test(void *dummy, tnf_probe_control_t *probe_p,
tnf_probe_setup_t *set_p)
{
tnf_ops_t *tpd_p;
tpd_p = tnf_probe_getfunc();
if (tpd_p) {
return (probe_p->alloc_func(tpd_p, probe_p, set_p));
}
return (NULL);
}
/*
* tnf_get_ops() returns the ops pointer (thread-private data), or NULL
* if tracing is disabled for this thread.
*/
static tnf_ops_t *
tnf_get_ops()
{
tnf_context_t *test_func_p = &thr_probe_getfunc_addr;
tnf_context_t test_func;
/*
* IMPORTANT: this test to see whether thr_probe_getfunc_addr
* is bound is tricky. The compiler currently has a bug
* (1263684) that causes the test to be optimized away unless
* coded with an intermediate pointer (test_func_p). This
* causes the process to SEGV when the variable is not bound.
*/
test_func = test_func_p ? *test_func_p : tnf_probe_getfunc;
return ((*test_func)());
}