[DTrace-devel] reread event from buffer after exit() action
Kris Van Hees
kris.van.hees at oracle.com
Thu Jun 4 21:15:40 PDT 2020
On Thu, Jun 04, 2020 at 02:09:02PM -0700, Eugene Loh wrote:
> Consider the following two runs:
>
> # dtrace -n 'BEGIN { trace(345); exit(0) }'
> dtrace: description 'BEGIN ' matched 1 probe
> CPU ID FUNCTION:NAME
> 2 1 :BEGIN 345
>
> # dtrace -n 'BEGIN { trace(345); exit(0) } write:entry'
> dtrace: description 'BEGIN ' matched 2 probes
> CPU ID FUNCTION:NAME
> 2 1 :BEGIN 345
> 2 1 :BEGIN 345
>
> The second one has doubled output. Unsurprisingly, legacy DTrace does
> not do this.
This is a known problem. Your analysis is very accurate and goes beyond where
I left off when I first encountered this issue. I didn't give it as much
attention yet because it ties into the semantics of the END probe as well,
which is known to not be entirely correct either right now.
> Here is what I think is happening. If we hit an exit() action,
> dt_consume_one() will return DONE:
>
> 2318 dt_consume_one(...) {
> 2397 for (i = 0; i < pdat->dtpda_ddesc->dtdd_nrecs; i++) {
> 2401 rec = &pdat->dtpda_ddesc->dtdd_recs[i];
> 2402 if (rec->dtrd_action == DTRACEACT_EXIT)
> 2403 done = 1;
> 2437 }
> 2447 return done ? DTRACE_WORKSTATUS_DONE : DTRACE_WORKSTATUS_OKAY;
> 2465 }
>
> That's fine, but then in dt_consume_cpu(), the non-OKAY return value at
> line 2538 means the read tail of the cyclic buffer is not updated (lines
> 2541 and 2544):
Consuming the record for the exit action without updating the tail of the
cyclic buffer is indeed a bug. We should never consume an event without
ensuring that we update the tail to mark it as processed.
> 2502 head = ring_buffer_read_head(rb_page);
> 2503 tail = rb_page->data_tail;
> 2505 if (head == tail)
> 2506 break;
> 2508 do {
> 2535 rval = dt_consume_one(...);
> 2538 if (rval != DTRACE_WORKSTATUS_OKAY)
> 2539 return rval;
> 2541 tail += hdr->size;
> 2542 } while (tail != head);
> 2544 ring_buffer_write_tail(rb_page, tail);
>
> Going up the food chain, we get to cmd/dtrace.c:
>
> 1515 do {
> 1528 if (done == DONE_SAW_EXIT) {
> 1530 done = DONE_SAW_END;
> 1531 dtrace_stop(g_dtp);
> 1533 }
> 1535 switch (dtrace_work(g_dtp, g_ofp, chew, chewrec, NULL)) {
> 1536 case DTRACE_WORKSTATUS_DONE:
> 1537 if (done != DONE_SAW_END)
> 1538 done = DONE_SAW_EXIT;
> 1539 break;
> 1540 case DTRACE_WORKSTATUS_OKAY:
> 1541 break;
> 1545 }
> 1549 } while (done != DONE_SAW_END);
>
> We call dtrace_work(), and the exit() action causes done=EXIT at line
> 1538. Then we cycle through the loop and at line 1530 set done=END. We
> call dtrace_work() again and reread the same BEGIN event since the read
> tail was never updated.
>
> I could imagine:
> *) updating the read tail of the cyclic buffer in dt_consume_cpu(), even
> if dt_consume_one() does not return OKAY
The only two cases I can imagine where not updating the tail of the cyclic
buffer would make sense are:
(1) if the record failed to be processed because it is incomplete. In view of
the design of the buffer handling on the producer end, it should never be
possible to have a partial record.
(2) if we see a record for the END probe and we have not yet processed buffers
for other CPUs. THe END probe should always be the last one reported, so
it is possible that we need to process the buffers for other CPUs first,
and then get back to the END probe record.
The interaction between END probe and exit() action is fascinating and complex.
Kris
More information about the DTrace-devel
mailing list