github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/gadgets/top/block-io/tracer/tracer.go (about)

     1  // Copyright 2023 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build !withoutebpf
    16  
    17  package tracer
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"time"
    24  	"unsafe"
    25  
    26  	"github.com/cilium/ebpf"
    27  	"github.com/cilium/ebpf/link"
    28  
    29  	"github.com/inspektor-gadget/inspektor-gadget/pkg/columns"
    30  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets"
    31  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/top"
    32  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/top/block-io/types"
    33  	"github.com/inspektor-gadget/inspektor-gadget/pkg/kallsyms"
    34  	eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types"
    35  )
    36  
    37  //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target $TARGET -type info_t -type val_t -cc clang -cflags ${CFLAGS} biotop ./bpf/biotop.bpf.c --
    38  
    39  type Config struct {
    40  	MaxRows    int
    41  	Interval   time.Duration
    42  	Iterations int
    43  	SortBy     []string
    44  	MountnsMap *ebpf.Map
    45  }
    46  
    47  type Tracer struct {
    48  	config           *Config
    49  	objs             biotopObjects
    50  	ioStartLink      link.Link
    51  	startRequestLink link.Link
    52  	doneLink         link.Link
    53  	enricher         gadgets.DataEnricherByMntNs
    54  	eventCallback    func(*top.Event[types.Stats])
    55  	done             chan bool
    56  	colMap           columns.ColumnMap[types.Stats]
    57  }
    58  
    59  func NewTracer(config *Config, enricher gadgets.DataEnricherByMntNs,
    60  	eventCallback func(*top.Event[types.Stats]),
    61  ) (*Tracer, error) {
    62  	t := &Tracer{
    63  		config:        config,
    64  		enricher:      enricher,
    65  		eventCallback: eventCallback,
    66  		done:          make(chan bool),
    67  	}
    68  
    69  	if err := t.install(); err != nil {
    70  		t.close()
    71  		return nil, err
    72  	}
    73  
    74  	statCols, err := columns.NewColumns[types.Stats]()
    75  	if err != nil {
    76  		t.close()
    77  		return nil, err
    78  	}
    79  	t.colMap = statCols.GetColumnMap()
    80  
    81  	go t.run(context.TODO())
    82  
    83  	return t, nil
    84  }
    85  
    86  // Stop stops the tracer
    87  // TODO: Remove after refactoring
    88  func (t *Tracer) Stop() {
    89  	t.close()
    90  }
    91  
    92  func (t *Tracer) close() {
    93  	close(t.done)
    94  
    95  	t.ioStartLink = gadgets.CloseLink(t.ioStartLink)
    96  	t.startRequestLink = gadgets.CloseLink(t.startRequestLink)
    97  	t.doneLink = gadgets.CloseLink(t.doneLink)
    98  
    99  	t.objs.Close()
   100  }
   101  
   102  func (t *Tracer) install() error {
   103  	spec, err := loadBiotop()
   104  	if err != nil {
   105  		return fmt.Errorf("loading ebpf program: %w", err)
   106  	}
   107  
   108  	if err := gadgets.LoadeBPFSpec(t.config.MountnsMap, spec, nil, &t.objs); err != nil {
   109  		return fmt.Errorf("loading ebpf spec: %w", err)
   110  	}
   111  
   112  	kernelSymbols, err := kallsyms.NewKAllSyms()
   113  	if err != nil {
   114  		return fmt.Errorf("loading kernel symbols: %w", err)
   115  	}
   116  
   117  	// __blk_account_io_start and __blk_account_io_done were inlined in:
   118  	// be6bfe36db17 ("block: inline hot paths of blk_account_io_*()").
   119  	// which was included in kernel 5.16.
   120  	// So let's be future proof and check if these symbols do not exist.
   121  	blkAccountIoStartFunction := "__blk_account_io_start"
   122  	if !kernelSymbols.SymbolExists(blkAccountIoStartFunction) {
   123  		blkAccountIoStartFunction = "blk_account_io_start"
   124  	}
   125  
   126  	blkAccountIoDoneFunction := "__blk_account_io_done"
   127  	if !kernelSymbols.SymbolExists(blkAccountIoDoneFunction) {
   128  		blkAccountIoDoneFunction = "blk_account_io_done"
   129  	}
   130  
   131  	t.ioStartLink, err = link.Kprobe(blkAccountIoStartFunction, t.objs.IgTopioStart, nil)
   132  	if err != nil {
   133  		return fmt.Errorf("attaching kprobe: %w", err)
   134  	}
   135  
   136  	t.startRequestLink, err = link.Kprobe("blk_mq_start_request", t.objs.IgTopioReq, nil)
   137  	if err != nil {
   138  		return fmt.Errorf("attaching kprobe: %w", err)
   139  	}
   140  
   141  	t.doneLink, err = link.Kprobe(blkAccountIoDoneFunction, t.objs.IgTopioDone, nil)
   142  	if err != nil {
   143  		return fmt.Errorf("attaching kprobe: %w", err)
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  func (t *Tracer) nextStats() ([]*types.Stats, error) {
   150  	stats := []*types.Stats{}
   151  
   152  	var prev *biotopInfoT = nil
   153  	key := biotopInfoT{}
   154  	counts := t.objs.Counts
   155  
   156  	defer func() {
   157  		// delete elements
   158  		err := counts.NextKey(nil, unsafe.Pointer(&key))
   159  		if err != nil {
   160  			return
   161  		}
   162  
   163  		for {
   164  			if err := counts.Delete(key); err != nil {
   165  				return
   166  			}
   167  
   168  			prev = &key
   169  			if err := counts.NextKey(unsafe.Pointer(prev), unsafe.Pointer(&key)); err != nil {
   170  				return
   171  			}
   172  		}
   173  	}()
   174  
   175  	// gather elements
   176  	err := counts.NextKey(nil, unsafe.Pointer(&key))
   177  	if err != nil {
   178  		if errors.Is(err, ebpf.ErrKeyNotExist) {
   179  			return stats, nil
   180  		}
   181  		return nil, fmt.Errorf("getting next key: %w", err)
   182  	}
   183  
   184  	for {
   185  		val := biotopValT{}
   186  		if err := counts.Lookup(key, unsafe.Pointer(&val)); err != nil {
   187  			return nil, err
   188  		}
   189  
   190  		stat := types.Stats{
   191  			Write:         key.Rwflag != 0,
   192  			Major:         int(key.Major),
   193  			Minor:         int(key.Minor),
   194  			WithMountNsID: eventtypes.WithMountNsID{MountNsID: key.Mntnsid},
   195  			Pid:           int32(key.Pid),
   196  			Comm:          gadgets.FromCString(key.Name[:]),
   197  			Bytes:         val.Bytes,
   198  			MicroSecs:     val.Us,
   199  			Operations:    val.Io,
   200  		}
   201  
   202  		if t.enricher != nil {
   203  			t.enricher.EnrichByMntNs(&stat.CommonData, stat.MountNsID)
   204  		}
   205  
   206  		stats = append(stats, &stat)
   207  
   208  		prev = &key
   209  		if err := counts.NextKey(unsafe.Pointer(prev), unsafe.Pointer(&key)); err != nil {
   210  			if errors.Is(err, ebpf.ErrKeyNotExist) {
   211  				break
   212  			}
   213  			return nil, fmt.Errorf("getting next key: %w", err)
   214  		}
   215  	}
   216  
   217  	top.SortStats(stats, t.config.SortBy, &t.colMap)
   218  
   219  	return stats, nil
   220  }
   221  
   222  func (t *Tracer) run(ctx context.Context) error {
   223  	// Don't use a context with a timeout but a counter to avoid having to deal
   224  	// with two timers: one for the timeout and another for the ticker.
   225  	count := t.config.Iterations
   226  	ticker := time.NewTicker(t.config.Interval)
   227  	defer ticker.Stop()
   228  
   229  	for {
   230  		select {
   231  		case <-t.done:
   232  			// TODO: Once we completely move to use Run instead of NewTracer,
   233  			// we can remove this as nobody will directly call Stop (cleanup).
   234  			return nil
   235  		case <-ctx.Done():
   236  			return nil
   237  		case <-ticker.C:
   238  			stats, err := t.nextStats()
   239  			if err != nil {
   240  				return fmt.Errorf("getting next stats: %w", err)
   241  			}
   242  
   243  			n := len(stats)
   244  			if n > t.config.MaxRows {
   245  				n = t.config.MaxRows
   246  			}
   247  			t.eventCallback(&top.Event[types.Stats]{Stats: stats[:n]})
   248  
   249  			// Count down only if user requested a finite number of iterations
   250  			// through a timeout.
   251  			if t.config.Iterations > 0 {
   252  				count--
   253  				if count == 0 {
   254  					return nil
   255  				}
   256  			}
   257  		}
   258  	}
   259  }
   260  
   261  func (t *Tracer) Run(gadgetCtx gadgets.GadgetContext) error {
   262  	if err := t.init(gadgetCtx); err != nil {
   263  		return fmt.Errorf("initializing tracer: %w", err)
   264  	}
   265  
   266  	defer t.close()
   267  	if err := t.install(); err != nil {
   268  		return fmt.Errorf("installing tracer: %w", err)
   269  	}
   270  
   271  	return t.run(gadgetCtx.Context())
   272  }
   273  
   274  func (t *Tracer) SetEventHandlerArray(handler any) {
   275  	nh, ok := handler.(func(ev []*types.Stats))
   276  	if !ok {
   277  		panic("event handler invalid")
   278  	}
   279  
   280  	// TODO: add errorHandler
   281  	t.eventCallback = func(ev *top.Event[types.Stats]) {
   282  		if ev.Error != "" {
   283  			return
   284  		}
   285  		nh(ev.Stats)
   286  	}
   287  }
   288  
   289  func (t *Tracer) SetMountNsMap(mntnsMap *ebpf.Map) {
   290  	t.config.MountnsMap = mntnsMap
   291  }
   292  
   293  func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) {
   294  	tracer := &Tracer{
   295  		config: &Config{
   296  			MaxRows:  20,
   297  			Interval: 1 * time.Second,
   298  			SortBy:   nil,
   299  		},
   300  		done: make(chan bool),
   301  	}
   302  	return tracer, nil
   303  }
   304  
   305  func (t *Tracer) init(gadgetCtx gadgets.GadgetContext) error {
   306  	params := gadgetCtx.GadgetParams()
   307  	t.config.MaxRows = params.Get(gadgets.ParamMaxRows).AsInt()
   308  	t.config.SortBy = params.Get(gadgets.ParamSortBy).AsStringSlice()
   309  	t.config.Interval = time.Second * time.Duration(params.Get(gadgets.ParamInterval).AsInt())
   310  
   311  	var err error
   312  	if t.config.Iterations, err = top.ComputeIterations(t.config.Interval, gadgetCtx.Timeout()); err != nil {
   313  		return err
   314  	}
   315  
   316  	statCols, err := columns.NewColumns[types.Stats]()
   317  	if err != nil {
   318  		return err
   319  	}
   320  	t.colMap = statCols.GetColumnMap()
   321  
   322  	return nil
   323  }