github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/fs2/io.go (about) 1 package fs2 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "os" 8 "strconv" 9 "strings" 10 11 "github.com/sirupsen/logrus" 12 13 "github.com/opencontainers/runc/libcontainer/cgroups" 14 "github.com/opencontainers/runc/libcontainer/configs" 15 ) 16 17 func isIoSet(r *configs.Resources) bool { 18 return r.BlkioWeight != 0 || 19 len(r.BlkioWeightDevice) > 0 || 20 len(r.BlkioThrottleReadBpsDevice) > 0 || 21 len(r.BlkioThrottleWriteBpsDevice) > 0 || 22 len(r.BlkioThrottleReadIOPSDevice) > 0 || 23 len(r.BlkioThrottleWriteIOPSDevice) > 0 24 } 25 26 // bfqDeviceWeightSupported checks for per-device BFQ weight support (added 27 // in kernel v5.4, commit 795fe54c2a8) by reading from "io.bfq.weight". 28 func bfqDeviceWeightSupported(bfq *os.File) bool { 29 if bfq == nil { 30 return false 31 } 32 _, _ = bfq.Seek(0, 0) 33 buf := make([]byte, 32) 34 _, _ = bfq.Read(buf) 35 // If only a single number (default weight) if read back, we have older kernel. 36 _, err := strconv.ParseInt(string(bytes.TrimSpace(buf)), 10, 64) 37 return err != nil 38 } 39 40 func setIo(dirPath string, r *configs.Resources) error { 41 if !isIoSet(r) { 42 return nil 43 } 44 45 // If BFQ IO scheduler is available, use it. 46 var bfq *os.File 47 if r.BlkioWeight != 0 || len(r.BlkioWeightDevice) > 0 { 48 var err error 49 bfq, err = cgroups.OpenFile(dirPath, "io.bfq.weight", os.O_RDWR) 50 if err == nil { 51 defer bfq.Close() 52 } else if !os.IsNotExist(err) { 53 return err 54 } 55 } 56 57 if r.BlkioWeight != 0 { 58 if bfq != nil { // Use BFQ. 59 if _, err := bfq.WriteString(strconv.FormatUint(uint64(r.BlkioWeight), 10)); err != nil { 60 return err 61 } 62 } else { 63 // Fallback to io.weight with a conversion scheme. 64 v := cgroups.ConvertBlkIOToIOWeightValue(r.BlkioWeight) 65 if err := cgroups.WriteFile(dirPath, "io.weight", strconv.FormatUint(v, 10)); err != nil { 66 return err 67 } 68 } 69 } 70 if bfqDeviceWeightSupported(bfq) { 71 for _, wd := range r.BlkioWeightDevice { 72 if _, err := bfq.WriteString(wd.WeightString() + "\n"); err != nil { 73 return fmt.Errorf("setting device weight %q: %w", wd.WeightString(), err) 74 } 75 } 76 } 77 for _, td := range r.BlkioThrottleReadBpsDevice { 78 if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("rbps")); err != nil { 79 return err 80 } 81 } 82 for _, td := range r.BlkioThrottleWriteBpsDevice { 83 if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("wbps")); err != nil { 84 return err 85 } 86 } 87 for _, td := range r.BlkioThrottleReadIOPSDevice { 88 if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("riops")); err != nil { 89 return err 90 } 91 } 92 for _, td := range r.BlkioThrottleWriteIOPSDevice { 93 if err := cgroups.WriteFile(dirPath, "io.max", td.StringName("wiops")); err != nil { 94 return err 95 } 96 } 97 98 return nil 99 } 100 101 func readCgroup2MapFile(dirPath string, name string) (map[string][]string, error) { 102 ret := map[string][]string{} 103 f, err := cgroups.OpenFile(dirPath, name, os.O_RDONLY) 104 if err != nil { 105 return nil, err 106 } 107 defer f.Close() 108 scanner := bufio.NewScanner(f) 109 for scanner.Scan() { 110 line := scanner.Text() 111 parts := strings.Fields(line) 112 if len(parts) < 2 { 113 continue 114 } 115 ret[parts[0]] = parts[1:] 116 } 117 if err := scanner.Err(); err != nil { 118 return nil, &parseError{Path: dirPath, File: name, Err: err} 119 } 120 return ret, nil 121 } 122 123 func statIo(dirPath string, stats *cgroups.Stats) error { 124 const file = "io.stat" 125 values, err := readCgroup2MapFile(dirPath, file) 126 if err != nil { 127 return err 128 } 129 // more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt 130 var parsedStats cgroups.BlkioStats 131 for k, v := range values { 132 d := strings.Split(k, ":") 133 if len(d) != 2 { 134 continue 135 } 136 major, err := strconv.ParseUint(d[0], 10, 64) 137 if err != nil { 138 return &parseError{Path: dirPath, File: file, Err: err} 139 } 140 minor, err := strconv.ParseUint(d[1], 10, 64) 141 if err != nil { 142 return &parseError{Path: dirPath, File: file, Err: err} 143 } 144 145 for _, item := range v { 146 d := strings.Split(item, "=") 147 if len(d) != 2 { 148 continue 149 } 150 op := d[0] 151 152 // Map to the cgroupv1 naming and layout (in separate tables). 153 var targetTable *[]cgroups.BlkioStatEntry 154 switch op { 155 // Equivalent to cgroupv1's blkio.io_service_bytes. 156 case "rbytes": 157 op = "Read" 158 targetTable = &parsedStats.IoServiceBytesRecursive 159 case "wbytes": 160 op = "Write" 161 targetTable = &parsedStats.IoServiceBytesRecursive 162 // Equivalent to cgroupv1's blkio.io_serviced. 163 case "rios": 164 op = "Read" 165 targetTable = &parsedStats.IoServicedRecursive 166 case "wios": 167 op = "Write" 168 targetTable = &parsedStats.IoServicedRecursive 169 default: 170 // Skip over entries we cannot map to cgroupv1 stats for now. 171 // In the future we should expand the stats struct to include 172 // them. 173 logrus.Debugf("cgroupv2 io stats: skipping over unmappable %s entry", item) 174 continue 175 } 176 177 value, err := strconv.ParseUint(d[1], 10, 64) 178 if err != nil { 179 return &parseError{Path: dirPath, File: file, Err: err} 180 } 181 182 entry := cgroups.BlkioStatEntry{ 183 Op: op, 184 Major: major, 185 Minor: minor, 186 Value: value, 187 } 188 *targetTable = append(*targetTable, entry) 189 } 190 } 191 stats.BlkioStats = parsedStats 192 return nil 193 }