| package fs2 |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "os" |
| "strconv" |
| "strings" |
| |
| "github.com/sirupsen/logrus" |
| |
| "github.com/opencontainers/runc/libcontainer/cgroups" |
| "github.com/opencontainers/runc/libcontainer/configs" |
| ) |
| |
| func isIoSet(r *configs.Resources) bool { |
| return r.BlkioWeight != 0 || |
| len(r.BlkioWeightDevice) > 0 || |
| len(r.BlkioThrottleReadBpsDevice) > 0 || |
| len(r.BlkioThrottleWriteBpsDevice) > 0 || |
| len(r.BlkioThrottleReadIOPSDevice) > 0 || |
| len(r.BlkioThrottleWriteIOPSDevice) > 0 |
| } |
| |
| // bfqDeviceWeightSupported checks for per-device BFQ weight support (added |
| // in kernel v5.4, commit 795fe54c2a8) by reading from "io.bfq.weight". |
| func bfqDeviceWeightSupported(bfq *os.File) bool { |
| if bfq == nil { |
| return false |
| } |
| _, _ = bfq.Seek(0, 0) |
| buf := make([]byte, 32) |
| _, _ = bfq.Read(buf) |
| // If only a single number (default weight) if read back, we have older kernel. |
| _, err := strconv.ParseInt(string(bytes.TrimSpace(buf)), 10, 64) |
| return err != nil |
| } |
| |
| func setIo(dirPath string, r *configs.Resources) error { |
| if !isIoSet(r) { |
| return nil |
| } |
| |
| // If BFQ IO scheduler is available, use it. |
| var bfq *os.File |
| if r.BlkioWeight != 0 || len(r.BlkioWeightDevice) > 0 { |
| var err error |
| bfq, err = cgroups.OpenFile(dirPath, "io.bfq.weight", os.O_RDWR) |
| if err == nil { |
| defer bfq.Close() |
| } else if !os.IsNotExist(err) { |
| return err |
| } |
| } |
| |
| if r.BlkioWeight != 0 { |
| if bfq != nil { // Use BFQ. |
| if _, err := bfq.WriteString(strconv.FormatUint(uint64(r.BlkioWeight), 10)); err != nil { |
| return err |
| } |
| } else { |
| // Fallback to io.weight with a conversion scheme. |
| v := cgroups.ConvertBlkIOToIOWeightValue(r.BlkioWeight) |
| if err := cgroups.WriteFile(dirPath, "io.weight", strconv.FormatUint(v, 10)); err != nil { |
| return err |
| } |
| } |
| } |
| if bfqDeviceWeightSupported(bfq) { |
| for _, wd := range r.BlkioWeightDevice { |
| if _, err := bfq.WriteString(wd.WeightString() + "\n"); err != nil { |
| return fmt.Errorf("setting device weight %q: %w", wd.WeightString(), err) |
| } |
| } |
| } |
| for _, td := range r.BlkioThrottleReadBpsDevice { |
| if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("rbps")); err != nil { |
| return err |
| } |
| } |
| for _, td := range r.BlkioThrottleWriteBpsDevice { |
| if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("wbps")); err != nil { |
| return err |
| } |
| } |
| for _, td := range r.BlkioThrottleReadIOPSDevice { |
| if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("riops")); err != nil { |
| return err |
| } |
| } |
| for _, td := range r.BlkioThrottleWriteIOPSDevice { |
| if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("wiops")); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| func readCgroup2MapFile(dirPath string, name string) (map[string][]string, error) { |
| ret := map[string][]string{} |
| f, err := cgroups.OpenFile(dirPath, name, os.O_RDONLY) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| scanner := bufio.NewScanner(f) |
| for scanner.Scan() { |
| line := scanner.Text() |
| parts := strings.Fields(line) |
| if len(parts) < 2 { |
| continue |
| } |
| ret[parts[0]] = parts[1:] |
| } |
| if err := scanner.Err(); err != nil { |
| return nil, &parseError{Path: dirPath, File: name, Err: err} |
| } |
| return ret, nil |
| } |
| |
| func statIo(dirPath string, stats *cgroups.Stats) error { |
| const file = "io.stat" |
| values, err := readCgroup2MapFile(dirPath, file) |
| if err != nil { |
| return err |
| } |
| // more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt |
| var parsedStats cgroups.BlkioStats |
| for k, v := range values { |
| d := strings.Split(k, ":") |
| if len(d) != 2 { |
| continue |
| } |
| major, err := strconv.ParseUint(d[0], 10, 64) |
| if err != nil { |
| return &parseError{Path: dirPath, File: file, Err: err} |
| } |
| minor, err := strconv.ParseUint(d[1], 10, 64) |
| if err != nil { |
| return &parseError{Path: dirPath, File: file, Err: err} |
| } |
| |
| for _, item := range v { |
| d := strings.Split(item, "=") |
| if len(d) != 2 { |
| continue |
| } |
| op := d[0] |
| |
| // Map to the cgroupv1 naming and layout (in separate tables). |
| var targetTable *[]cgroups.BlkioStatEntry |
| switch op { |
| // Equivalent to cgroupv1's blkio.io_service_bytes. |
| case "rbytes": |
| op = "Read" |
| targetTable = &parsedStats.IoServiceBytesRecursive |
| case "wbytes": |
| op = "Write" |
| targetTable = &parsedStats.IoServiceBytesRecursive |
| // Equivalent to cgroupv1's blkio.io_serviced. |
| case "rios": |
| op = "Read" |
| targetTable = &parsedStats.IoServicedRecursive |
| case "wios": |
| op = "Write" |
| targetTable = &parsedStats.IoServicedRecursive |
| default: |
| // Skip over entries we cannot map to cgroupv1 stats for now. |
| // In the future we should expand the stats struct to include |
| // them. |
| logrus.Debugf("cgroupv2 io stats: skipping over unmappable %s entry", item) |
| continue |
| } |
| |
| value, err := strconv.ParseUint(d[1], 10, 64) |
| if err != nil { |
| return &parseError{Path: dirPath, File: file, Err: err} |
| } |
| |
| entry := cgroups.BlkioStatEntry{ |
| Op: op, |
| Major: major, |
| Minor: minor, |
| Value: value, |
| } |
| *targetTable = append(*targetTable, entry) |
| } |
| } |
| stats.BlkioStats = parsedStats |
| return nil |
| } |