| // Licensed to Apache Software Foundation (ASF) under one or more contributor |
| // license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright |
| // ownership. Apache Software Foundation (ASF) licenses this file to you under |
| // the Apache License, Version 2.0 (the "License"); you may |
| // not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| package process |
| |
| import ( |
| "bufio" |
| "fmt" |
| "os" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/apache/skywalking-rover/pkg/logger" |
| host2 "github.com/apache/skywalking-rover/pkg/tools/host" |
| "github.com/apache/skywalking-rover/pkg/tools/path" |
| "github.com/apache/skywalking-rover/pkg/tools/profiling" |
| ) |
| |
| var ( |
| // NotSupportProfilingExe mean which program are not support for profiling |
| // Not Support JIT and Script language for now |
| NotSupportProfilingExe = []string{ |
| "java", "python", "node", "bash", "ruby", "ssh", |
| } |
| |
| // executable file profiling finders |
| profilingStatFinderList = []profiling.StatFinder{ |
| profiling.NewGoLibrary(), |
| } |
| |
| // kernel profiling finder |
| kernelFinder = profiling.NewKernelFinder() |
| |
| // process map file analyze(/proc/{pid}/maps) |
| mapFileContentRegex = regexp.MustCompile("(?P<StartAddr>[a-f\\d]+)\\-(?P<EndAddr>[a-f\\d]+)\\s(?P<Perm>[^\\s]+)" + |
| "\\s(?P<Offset>[a-f\\d]+)\\s[a-f\\d]+\\:[a-f\\d]+\\s\\d+\\s+(?P<Name>[^\\n]+)") |
| |
| log = logger.GetLogger("tools", "process") |
| ) |
| |
| // KernelFileProfilingStat is works for read the kernel and get is support for kernel symbol analyze |
| func KernelFileProfilingStat() (*profiling.Info, error) { |
| if !kernelFinder.IsSupport(profiling.KernelProcSymbolFilePath) { |
| return nil, fmt.Errorf("not support kernel space profiling") |
| } |
| return kernelFinder.Analyze(profiling.KernelProcSymbolFilePath) |
| } |
| |
| // SupportProfiling reports whether the executable can be profiled (not excluded and has a usable |
| // symbol table) WITHOUT retaining the parsed symbol/module data in memory. Use this for the |
| // discovery-time "support_ebpf_profiling" flag: the heavy symbol data is only needed while an |
| // actual profiling task runs and is built on demand by ProfilingStat. |
| func SupportProfiling(exePath string) (bool, error) { |
| stat, err := os.Stat(exePath) |
| if err != nil { |
| return false, fmt.Errorf("check file error: %v", err) |
| } |
| for _, notSupport := range NotSupportProfilingExe { |
| if strings.HasPrefix(stat.Name(), notSupport) { |
| return false, fmt.Errorf("not support %s language profiling", notSupport) |
| } |
| } |
| |
| // the executable file must have the symbols; the parsed symbols are discarded here so they |
| // are not retained in memory for every discovered process. |
| symbols, err := newAnalyzeContext().GetFinder(exePath).AnalyzeSymbols(exePath) |
| if err != nil { |
| return false, fmt.Errorf("could not analyze symbols in the executable file %s: %w", exePath, err) |
| } |
| if len(symbols) == 0 { |
| return false, fmt.Errorf("could not find any symbol in the executable file: %s", exePath) |
| } |
| return true, nil |
| } |
| |
| // ProfilingStat is validating the exe file could be profiling and get info |
| func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) { |
| if support, err := SupportProfiling(exePath); !support { |
| return nil, err |
| } |
| |
| return analyzeProfilingInfo(newAnalyzeContext(), pid) |
| } |
| |
| // Modules Read the profiling info of the process, without the symbol check |
| func Modules(pid int32) ([]*profiling.Module, error) { |
| context := newAnalyzeContext() |
| info, err := analyzeProfilingInfo(context, pid) |
| if err != nil { |
| return nil, err |
| } |
| return info.Modules, nil |
| } |
| |
| func analyzeProfilingInfo(context *analyzeContext, pid int32) (*profiling.Info, error) { |
| // analyze process mapping |
| mapFile, _ := os.Open(host2.GetHostProcInHost(fmt.Sprintf("%d/maps", pid))) |
| scanner := bufio.NewScanner(mapFile) |
| modules := make(map[string]*profiling.Module) |
| for scanner.Scan() { |
| submatch := mapFileContentRegex.FindStringSubmatch(scanner.Text()) |
| if len(submatch) != 6 { |
| continue |
| } |
| if len(submatch[3]) > 2 && submatch[3][2] != 'x' { |
| continue |
| } |
| moduleName := submatch[5] |
| if isIgnoreModuleName(moduleName) { |
| continue |
| } |
| |
| // parsing range |
| var err error |
| moduleRange := &profiling.ModuleRange{} |
| moduleRange.StartAddr, err = parseUInt64InModule(err, moduleName, "start address", submatch[1]) |
| moduleRange.EndAddr, err = parseUInt64InModule(err, moduleName, "end address", submatch[2]) |
| moduleRange.FileOffset, err = parseUInt64InModule(err, moduleName, "file offset", submatch[4]) |
| if err != nil { |
| return nil, err |
| } |
| |
| module := modules[moduleName] |
| if module != nil { |
| module.Ranges = append(module.Ranges, moduleRange) |
| continue |
| } |
| modulePath := host2.GetHostProcInHost(fmt.Sprintf("%d/root%s", pid, moduleName)) |
| if !path.Exists(modulePath) { |
| log.Debugf("could not found the module, ignore. name: %s, path: %s", moduleName, modulePath) |
| continue |
| } |
| |
| module, err = context.GetFinder(modulePath).ToModule(pid, moduleName, modulePath, []*profiling.ModuleRange{moduleRange}) |
| if err != nil { |
| if err == profiling.ErrNotSupport { |
| log.Warnf("not support the module in process(%d): %s, path: %s", pid, moduleName, modulePath) |
| continue |
| } |
| return nil, fmt.Errorf("could not init the module: %s, error: %v", moduleName, err) |
| } |
| modules[moduleName] = module |
| } |
| return profiling.NewInfo(modules), nil |
| } |
| |
| func parseUInt64InModule(err error, moduleName, key, val string) (uint64, error) { |
| if err != nil { |
| return 0, err |
| } |
| res, err := strconv.ParseUint(val, 16, 64) |
| if err != nil { |
| return 0, fmt.Errorf("error parsing the %s in maps file: %s", key, moduleName) |
| } |
| return res, nil |
| } |
| |
| func isIgnoreModuleName(name string) bool { |
| return name != "" && |
| (strings.HasPrefix(name, "//anon") || |
| strings.HasPrefix(name, "/dev/zero") || |
| strings.HasPrefix(name, "/anon_hugepage") || |
| strings.HasPrefix(name, "[stack") || |
| strings.HasPrefix(name, "/SYSV") || |
| strings.HasPrefix(name, "[heap]") || |
| strings.HasPrefix(name, "/memfd:") || |
| strings.HasPrefix(name, "[vdso]") || |
| strings.HasPrefix(name, "[vsyscall]") || |
| strings.HasPrefix(name, "[uprobes]") || |
| strings.HasSuffix(name, ".map")) |
| } |
| |
| type analyzeContext struct { |
| pathToFinder map[string]profiling.StatFinder |
| } |
| |
| func newAnalyzeContext() *analyzeContext { |
| return &analyzeContext{ |
| pathToFinder: make(map[string]profiling.StatFinder), |
| } |
| } |
| |
| func (a *analyzeContext) GetFinder(name string) profiling.StatFinder { |
| if f := a.pathToFinder[name]; f != nil { |
| return f |
| } |
| |
| // find all finders |
| for _, f := range profilingStatFinderList { |
| if f.IsSupport(name) { |
| a.pathToFinder[name] = f |
| return f |
| } |
| } |
| |
| // not support |
| n := profiling.NewNotSupport() |
| a.pathToFinder[name] = n |
| return n |
| } |