├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Protobuild.toml ├── README.md ├── cgroup1 ├── blkio.go ├── blkio_test.go ├── cgroup.go ├── cgroup_test.go ├── control.go ├── cpu.go ├── cpuacct.go ├── cpuacct_test.go ├── cpuset.go ├── devices.go ├── errors.go ├── freezer.go ├── hierarchy.go ├── hugetlb.go ├── memory.go ├── memory_test.go ├── mock_test.go ├── named.go ├── named_test.go ├── net_cls.go ├── net_prio.go ├── opts.go ├── paths.go ├── paths_test.go ├── perf_event.go ├── pids.go ├── pids_test.go ├── rdma.go ├── state.go ├── stats │ ├── doc.go │ ├── metrics.pb.go │ ├── metrics.pb.txt │ └── metrics.proto ├── subsystem.go ├── systemd.go ├── testutil_test.go ├── ticks.go ├── utils.go ├── utils_test.go └── v1.go ├── cgroup2 ├── cpu.go ├── cpuv2_test.go ├── devicefilter.go ├── devicefilter_test.go ├── ebpf.go ├── errors.go ├── hugetlb.go ├── hugetlbv2_test.go ├── io.go ├── iov2_test.go ├── manager.go ├── manager_test.go ├── memory.go ├── memoryv2_test.go ├── paths.go ├── paths_test.go ├── pids.go ├── pidsv2_test.go ├── rdma.go ├── state.go ├── stats │ ├── doc.go │ ├── metrics.pb.go │ ├── metrics.pb.txt │ └── metrics.proto ├── testutils_test.go ├── utils.go └── utils_test.go ├── cmd ├── cgctl │ └── main.go ├── go.mod └── go.sum ├── go.mod ├── go.sum ├── utils.go └── utils_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - 'release/**' 7 | pull_request: 8 | branches: 9 | - main 10 | - 'release/**' 11 | 12 | jobs: 13 | # 14 | # Project checks 15 | # 16 | project: 17 | name: Project Checks 18 | runs-on: ubuntu-22.04 19 | timeout-minutes: 5 20 | 21 | steps: 22 | # 23 | # Checkout repos 24 | # 25 | - name: Checkout cgroups 26 | uses: actions/checkout@v4 27 | with: 28 | path: src/github.com/containerd/cgroups 29 | fetch-depth: 25 30 | 31 | # 32 | # Install Go 33 | # 34 | - name: Install Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version: '1.22.x' 38 | cache-dependency-path: src/github.com/containerd/cgroups 39 | 40 | - name: Project checks 41 | uses: containerd/project-checks@v1.2.2 42 | with: 43 | working-directory: src/github.com/containerd/cgroups 44 | 45 | lint: 46 | name: Lint 47 | timeout-minutes: 10 48 | needs: [project] 49 | runs-on: ubuntu-22.04 50 | 51 | strategy: 52 | matrix: 53 | go-version: [1.22.x, 1.23.x] 54 | 55 | steps: 56 | - name: Checkout cgroups 57 | uses: actions/checkout@v4 58 | with: 59 | path: src/github.com/containerd/cgroups 60 | 61 | - name: Install Go 62 | uses: actions/setup-go@v5 63 | with: 64 | go-version: ${{ matrix.go-version }} 65 | cache-dependency-path: src/github.com/containerd/cgroups 66 | 67 | - name: golangci-lint 68 | uses: golangci/golangci-lint-action@v6 69 | with: 70 | version: v1.62.0 71 | args: --verbose 72 | working-directory: src/github.com/containerd/cgroups 73 | 74 | test: 75 | name: Test cgroups 76 | timeout-minutes: 15 77 | needs: [project] 78 | 79 | strategy: 80 | matrix: 81 | go-version: [1.22.x, 1.23.x] 82 | # Ubuntu-20.04 has cgroups v1 default; Ubuntu-22.04 has cgroups v2 default. 83 | os: [ubuntu-20.04, ubuntu-22.04] 84 | 85 | runs-on: ${{ matrix.os }} 86 | steps: 87 | - name: Checkout cgroups 88 | uses: actions/checkout@v4 89 | with: 90 | path: src/github.com/containerd/cgroups 91 | 92 | - name: Install Go 93 | uses: actions/setup-go@v5 94 | with: 95 | go-version: ${{ matrix.go-version }} 96 | # Disable Go caching feature when compiling across Linux distributions due to collisions until https://github.com/actions/setup-go/issues/368 is resolved. 97 | cache: false 98 | 99 | - name: Run cgroup tests 100 | run: | 101 | $(command -v go) test -exec sudo -v -race -coverprofile=coverage.txt -covermode=atomic ./... 102 | working-directory: src/github.com/containerd/cgroups 103 | 104 | - name: Build cgctl 105 | run: make all 106 | working-directory: src/github.com/containerd/cgroups 107 | 108 | proto: 109 | name: Compare auto-generated Go files 110 | runs-on: ubuntu-22.04 111 | 112 | steps: 113 | - name: Checkout cgroups 114 | uses: actions/checkout@v4 115 | with: 116 | path: src/github.com/containerd/cgroups 117 | 118 | - name: Install Go 119 | uses: actions/setup-go@v5 120 | with: 121 | go-version: '1.22.x' 122 | cache-dependency-path: src/github.com/containerd/cgroups 123 | 124 | - name: Set env 125 | shell: bash 126 | run: | 127 | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 128 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 129 | 130 | - name: Install protoc 131 | run: | 132 | curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.5/protoc-21.5-linux-x86_64.zip 133 | # /usr/local is not writable from GitHub Actions' user 134 | sudo unzip protoc-21.5-linux-x86_64.zip -d /usr/local 135 | 136 | - name: Install proto-related tools for Go 137 | run: | 138 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 139 | go install github.com/containerd/protobuild@v0.3.0 140 | go install github.com/containerd/protobuild/cmd/go-fix-acronym@v0.3.0 141 | 142 | - name: Compare auto-generated Go files 143 | run: | 144 | make proto 145 | git diff --exit-code 146 | working-directory: src/github.com/containerd/cgroups 147 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example/example 2 | cmd/cgctl/cgctl 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright The containerd 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 | PACKAGES=$(shell go list ./... | grep -v /vendor/) 16 | GO_TAGS=$(if $(GO_BUILDTAGS),-tags "$(strip $(GO_BUILDTAGS))",) 17 | GO ?= go 18 | GO_BUILD_FLAGS ?= 19 | 20 | all: cgutil 21 | $(GO) build -v $(GO_TAGS) 22 | 23 | cgutil: 24 | cd cmd/cgctl && $(GO) build $(GO_BUILD_FLAGS) -v $(GO_TAGS) 25 | 26 | proto: 27 | protobuild --quiet ${PACKAGES} 28 | # Keep them Go-idiomatic and backward-compatible with the gogo/protobuf era. 29 | go-fix-acronym -w -a '(Cpu|Tcp|Rss|Psi)' $(shell find cgroup1/stats/ cgroup2/stats/ -name '*.pb.go') 30 | -------------------------------------------------------------------------------- /Protobuild.toml: -------------------------------------------------------------------------------- 1 | version = "2" 2 | generators = ["go"] 3 | 4 | # Control protoc include paths. Below are usually some good defaults, but feel 5 | # free to try it without them if it works for your project. 6 | [includes] 7 | # Include paths that will be added before all others. Typically, you want to 8 | # treat the root of the project as an include, but this may not be necessary. 9 | # before = ["."] 10 | 11 | # Paths that will be added untouched to the end of the includes. We use 12 | # `/usr/local/include` to pickup the common install location of protobuf. 13 | # This is the default. 14 | after = ["/usr/local/include", "/usr/include"] 15 | 16 | # Aggregate the API descriptors to lock down API changes. 17 | [[descriptors]] 18 | prefix = "github.com/containerd/cgroups/cgroup1/stats" 19 | target = "cgroup1/stats/metrics.pb.txt" 20 | ignore_files = [ 21 | "google/protobuf/descriptor.proto", 22 | ] 23 | [[descriptors]] 24 | prefix = "github.com/containerd/cgroups/cgroup2/stats" 25 | target = "cgroup2/stats/metrics.pb.txt" 26 | ignore_files = [ 27 | "google/protobuf/descriptor.proto", 28 | ] 29 | 30 | [parameters.go] 31 | paths = "source_relative" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cgroups 2 | 3 | [![Build Status](https://github.com/containerd/cgroups/workflows/CI/badge.svg)](https://github.com/containerd/cgroups/actions?query=workflow%3ACI) 4 | [![codecov](https://codecov.io/gh/containerd/cgroups/branch/main/graph/badge.svg)](https://codecov.io/gh/containerd/cgroups) 5 | [![GoDoc](https://godoc.org/github.com/containerd/cgroups?status.svg)](https://godoc.org/github.com/containerd/cgroups) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/containerd/cgroups)](https://goreportcard.com/report/github.com/containerd/cgroups) 7 | 8 | Go package for creating, managing, inspecting, and destroying cgroups. 9 | The resources format for settings on the cgroup uses the OCI runtime-spec found 10 | [here](https://github.com/opencontainers/runtime-spec). 11 | 12 | ## Examples (v1) 13 | 14 | ### Create a new cgroup 15 | 16 | This creates a new cgroup using a static path for all subsystems under `/test`. 17 | 18 | * /sys/fs/cgroup/cpu/test 19 | * /sys/fs/cgroup/memory/test 20 | * etc.... 21 | 22 | It uses a single hierarchy and specifies cpu shares as a resource constraint and 23 | uses the v1 implementation of cgroups. 24 | 25 | 26 | ```go 27 | shares := uint64(100) 28 | control, err := cgroup1.New(cgroup1.StaticPath("/test"), &specs.LinuxResources{ 29 | CPU: &specs.LinuxCPU{ 30 | Shares: &shares, 31 | }, 32 | }) 33 | defer control.Delete() 34 | ``` 35 | 36 | ### Create with systemd slice support 37 | 38 | 39 | ```go 40 | control, err := cgroup1.New(cgroup1.Systemd, cgroup1.Slice("system.slice", "runc-test"), &specs.LinuxResources{ 41 | CPU: &specs.CPU{ 42 | Shares: &shares, 43 | }, 44 | }) 45 | 46 | ``` 47 | 48 | ### Load an existing cgroup 49 | 50 | ```go 51 | control, err = cgroup1.Load(cgroup1.Default, cgroups.StaticPath("/test")) 52 | ``` 53 | 54 | ### Add a process to the cgroup 55 | 56 | ```go 57 | if err := control.Add(cgroup1.Process{Pid:1234}); err != nil { 58 | } 59 | ``` 60 | 61 | ### Update the cgroup 62 | 63 | To update the resources applied in the cgroup 64 | 65 | ```go 66 | shares = uint64(200) 67 | if err := control.Update(&specs.LinuxResources{ 68 | CPU: &specs.LinuxCPU{ 69 | Shares: &shares, 70 | }, 71 | }); err != nil { 72 | } 73 | ``` 74 | 75 | ### Freeze and Thaw the cgroup 76 | 77 | ```go 78 | if err := control.Freeze(); err != nil { 79 | } 80 | if err := control.Thaw(); err != nil { 81 | } 82 | ``` 83 | 84 | ### List all processes in the cgroup or recursively 85 | 86 | ```go 87 | processes, err := control.Processes(cgroup1.Devices, recursive) 88 | ``` 89 | 90 | ### Get Stats on the cgroup 91 | 92 | ```go 93 | stats, err := control.Stat() 94 | ``` 95 | 96 | By adding `cgroups.IgnoreNotExist` all non-existent files will be ignored, e.g. swap memory stats without swap enabled 97 | ```go 98 | stats, err := control.Stat(cgroup1.IgnoreNotExist) 99 | ``` 100 | 101 | ### Move process across cgroups 102 | 103 | This allows you to take processes from one cgroup and move them to another. 104 | 105 | ```go 106 | err := control.MoveTo(destination) 107 | ``` 108 | 109 | ### Create subcgroup 110 | 111 | ```go 112 | subCgroup, err := control.New("child", resources) 113 | ``` 114 | 115 | ### Registering for memory events 116 | 117 | This allows you to get notified by an eventfd for v1 memory cgroups events. 118 | 119 | ```go 120 | event := cgroup1.MemoryThresholdEvent(50 * 1024 * 1024, false) 121 | efd, err := control.RegisterMemoryEvent(event) 122 | ``` 123 | 124 | ```go 125 | event := cgroup1.MemoryPressureEvent(cgroup1.MediumPressure, cgroup1.DefaultMode) 126 | efd, err := control.RegisterMemoryEvent(event) 127 | ``` 128 | 129 | ```go 130 | efd, err := control.OOMEventFD() 131 | // or by using RegisterMemoryEvent 132 | event := cgroup1.OOMEvent() 133 | efd, err := control.RegisterMemoryEvent(event) 134 | ``` 135 | 136 | ## Examples (v2/unified) 137 | 138 | ### Check that the current system is running cgroups v2 139 | 140 | ```go 141 | var cgroupV2 bool 142 | if cgroups.Mode() == cgroups.Unified { 143 | cgroupV2 = true 144 | } 145 | ``` 146 | 147 | ### Create a new cgroup 148 | 149 | This creates a new systemd v2 cgroup slice. Systemd slices consider ["-" a special character](https://www.freedesktop.org/software/systemd/man/systemd.slice.html), 150 | so the resulting slice would be located here on disk: 151 | 152 | * /sys/fs/cgroup/my.slice/my-cgroup.slice/my-cgroup-abc.slice 153 | 154 | ```go 155 | import ( 156 | "github.com/containerd/cgroups/v3/cgroup2" 157 | specs "github.com/opencontainers/runtime-spec/specs-go" 158 | ) 159 | 160 | res := cgroup2.Resources{} 161 | // dummy PID of -1 is used for creating a "general slice" to be used as a parent cgroup. 162 | // see https://github.com/containerd/cgroups/blob/1df78138f1e1e6ee593db155c6b369466f577651/v2/manager.go#L732-L735 163 | m, err := cgroup2.NewSystemd("/", "my-cgroup-abc.slice", -1, &res) 164 | if err != nil { 165 | return err 166 | } 167 | ``` 168 | 169 | ### Load an existing cgroup 170 | 171 | ```go 172 | m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice") 173 | if err != nil { 174 | return err 175 | } 176 | ``` 177 | 178 | ### Delete a cgroup 179 | 180 | ```go 181 | m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice") 182 | if err != nil { 183 | return err 184 | } 185 | err = m.DeleteSystemd() 186 | if err != nil { 187 | return err 188 | } 189 | ``` 190 | 191 | ### Kill all processes in a cgroup 192 | 193 | ```go 194 | m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice") 195 | if err != nil { 196 | return err 197 | } 198 | err = m.Kill() 199 | if err != nil { 200 | return err 201 | } 202 | ``` 203 | 204 | 205 | ### Get and set cgroup type 206 | ```go 207 | m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice") 208 | if err != nil { 209 | return err 210 | } 211 | 212 | // https://www.kernel.org/doc/html/v5.0/admin-guide/cgroup-v2.html#threads 213 | cgType, err := m.GetType() 214 | if err != nil { 215 | return err 216 | } 217 | fmt.Println(cgType) 218 | 219 | err = m.SetType(cgroup2.Threaded) 220 | if err != nil { 221 | return err 222 | } 223 | ``` 224 | 225 | ### Attention 226 | 227 | All static path should not include `/sys/fs/cgroup/` prefix, it should start with your own cgroups name 228 | 229 | ## Project details 230 | 231 | Cgroups is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). 232 | As a containerd sub-project, you will find the: 233 | 234 | * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), 235 | * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), 236 | * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) 237 | 238 | information in our [`containerd/project`](https://github.com/containerd/project) repository. 239 | -------------------------------------------------------------------------------- /cgroup1/blkio.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bufio" 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | 28 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 29 | 30 | specs "github.com/opencontainers/runtime-spec/specs-go" 31 | ) 32 | 33 | // NewBlkio returns a Blkio controller given the root folder of cgroups. 34 | // It may optionally accept other configuration options, such as ProcRoot(path) 35 | func NewBlkio(root string, options ...func(controller *blkioController)) *blkioController { 36 | ctrl := &blkioController{ 37 | root: filepath.Join(root, string(Blkio)), 38 | procRoot: "/proc", 39 | } 40 | for _, opt := range options { 41 | opt(ctrl) 42 | } 43 | return ctrl 44 | } 45 | 46 | // ProcRoot overrides the default location of the "/proc" filesystem 47 | func ProcRoot(path string) func(controller *blkioController) { 48 | return func(c *blkioController) { 49 | c.procRoot = path 50 | } 51 | } 52 | 53 | type blkioController struct { 54 | root string 55 | procRoot string 56 | } 57 | 58 | func (b *blkioController) Name() Name { 59 | return Blkio 60 | } 61 | 62 | func (b *blkioController) Path(path string) string { 63 | return filepath.Join(b.root, path) 64 | } 65 | 66 | func (b *blkioController) Create(path string, resources *specs.LinuxResources) error { 67 | if err := os.MkdirAll(b.Path(path), defaultDirPerm); err != nil { 68 | return err 69 | } 70 | if resources.BlockIO == nil { 71 | return nil 72 | } 73 | for _, t := range createBlkioSettings(resources.BlockIO) { 74 | if t.value != nil { 75 | if err := os.WriteFile( 76 | filepath.Join(b.Path(path), "blkio."+t.name), 77 | t.format(t.value), 78 | defaultFilePerm, 79 | ); err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | func (b *blkioController) Update(path string, resources *specs.LinuxResources) error { 88 | return b.Create(path, resources) 89 | } 90 | 91 | func (b *blkioController) Stat(path string, stats *v1.Metrics) error { 92 | stats.Blkio = &v1.BlkIOStat{} 93 | 94 | var settings []blkioStatSettings 95 | 96 | // Try to read CFQ stats available on all CFQ enabled kernels first 97 | if _, err := os.Lstat(filepath.Join(b.Path(path), "blkio.io_serviced_recursive")); err == nil { 98 | settings = []blkioStatSettings{ 99 | { 100 | name: "sectors_recursive", 101 | entry: &stats.Blkio.SectorsRecursive, 102 | }, 103 | { 104 | name: "io_service_bytes_recursive", 105 | entry: &stats.Blkio.IoServiceBytesRecursive, 106 | }, 107 | { 108 | name: "io_serviced_recursive", 109 | entry: &stats.Blkio.IoServicedRecursive, 110 | }, 111 | { 112 | name: "io_queued_recursive", 113 | entry: &stats.Blkio.IoQueuedRecursive, 114 | }, 115 | { 116 | name: "io_service_time_recursive", 117 | entry: &stats.Blkio.IoServiceTimeRecursive, 118 | }, 119 | { 120 | name: "io_wait_time_recursive", 121 | entry: &stats.Blkio.IoWaitTimeRecursive, 122 | }, 123 | { 124 | name: "io_merged_recursive", 125 | entry: &stats.Blkio.IoMergedRecursive, 126 | }, 127 | { 128 | name: "time_recursive", 129 | entry: &stats.Blkio.IoTimeRecursive, 130 | }, 131 | } 132 | } 133 | 134 | f, err := os.Open(filepath.Join(b.procRoot, "partitions")) 135 | if err != nil { 136 | return err 137 | } 138 | defer f.Close() 139 | 140 | devices, err := getDevices(f) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | var size int 146 | for _, t := range settings { 147 | if err := b.readEntry(devices, path, t.name, t.entry); err != nil { 148 | return err 149 | } 150 | size += len(*t.entry) 151 | } 152 | if size > 0 { 153 | return nil 154 | } 155 | 156 | // Even the kernel is compiled with the CFQ scheduler, the cgroup may not use 157 | // block devices with the CFQ scheduler. If so, we should fallback to throttle.* files. 158 | settings = []blkioStatSettings{ 159 | { 160 | name: "throttle.io_serviced", 161 | entry: &stats.Blkio.IoServicedRecursive, 162 | }, 163 | { 164 | name: "throttle.io_service_bytes", 165 | entry: &stats.Blkio.IoServiceBytesRecursive, 166 | }, 167 | } 168 | for _, t := range settings { 169 | if err := b.readEntry(devices, path, t.name, t.entry); err != nil { 170 | return err 171 | } 172 | } 173 | return nil 174 | } 175 | 176 | func (b *blkioController) readEntry(devices map[deviceKey]string, path, name string, entry *[]*v1.BlkIOEntry) error { 177 | f, err := os.Open(filepath.Join(b.Path(path), "blkio."+name)) 178 | if err != nil { 179 | return err 180 | } 181 | defer f.Close() 182 | sc := bufio.NewScanner(f) 183 | for sc.Scan() { 184 | // format: dev type amount 185 | fields := strings.FieldsFunc(sc.Text(), splitBlkIOStatLine) 186 | if len(fields) < 3 { 187 | if len(fields) == 2 && fields[0] == "Total" { 188 | // skip total line 189 | continue 190 | } else { 191 | return fmt.Errorf("invalid line found while parsing %s: %s", path, sc.Text()) 192 | } 193 | } 194 | major, err := strconv.ParseUint(fields[0], 10, 64) 195 | if err != nil { 196 | return err 197 | } 198 | minor, err := strconv.ParseUint(fields[1], 10, 64) 199 | if err != nil { 200 | return err 201 | } 202 | op := "" 203 | valueField := 2 204 | if len(fields) == 4 { 205 | op = fields[2] 206 | valueField = 3 207 | } 208 | v, err := strconv.ParseUint(fields[valueField], 10, 64) 209 | if err != nil { 210 | return err 211 | } 212 | *entry = append(*entry, &v1.BlkIOEntry{ 213 | Device: devices[deviceKey{major, minor}], 214 | Major: major, 215 | Minor: minor, 216 | Op: op, 217 | Value: v, 218 | }) 219 | } 220 | return sc.Err() 221 | } 222 | 223 | func createBlkioSettings(blkio *specs.LinuxBlockIO) []blkioSettings { 224 | settings := []blkioSettings{} 225 | 226 | if blkio.Weight != nil { 227 | settings = append(settings, 228 | blkioSettings{ 229 | name: "weight", 230 | value: blkio.Weight, 231 | format: uintf, 232 | }) 233 | } 234 | if blkio.LeafWeight != nil { 235 | settings = append(settings, 236 | blkioSettings{ 237 | name: "leaf_weight", 238 | value: blkio.LeafWeight, 239 | format: uintf, 240 | }) 241 | } 242 | for _, wd := range blkio.WeightDevice { 243 | if wd.Weight != nil { 244 | settings = append(settings, 245 | blkioSettings{ 246 | name: "weight_device", 247 | value: wd, 248 | format: weightdev, 249 | }) 250 | } 251 | if wd.LeafWeight != nil { 252 | settings = append(settings, 253 | blkioSettings{ 254 | name: "leaf_weight_device", 255 | value: wd, 256 | format: weightleafdev, 257 | }) 258 | } 259 | } 260 | for _, t := range []struct { 261 | name string 262 | list []specs.LinuxThrottleDevice 263 | }{ 264 | { 265 | name: "throttle.read_bps_device", 266 | list: blkio.ThrottleReadBpsDevice, 267 | }, 268 | { 269 | name: "throttle.read_iops_device", 270 | list: blkio.ThrottleReadIOPSDevice, 271 | }, 272 | { 273 | name: "throttle.write_bps_device", 274 | list: blkio.ThrottleWriteBpsDevice, 275 | }, 276 | { 277 | name: "throttle.write_iops_device", 278 | list: blkio.ThrottleWriteIOPSDevice, 279 | }, 280 | } { 281 | for _, td := range t.list { 282 | settings = append(settings, blkioSettings{ 283 | name: t.name, 284 | value: td, 285 | format: throttleddev, 286 | }) 287 | } 288 | } 289 | return settings 290 | } 291 | 292 | type blkioSettings struct { 293 | name string 294 | value interface{} 295 | format func(v interface{}) []byte 296 | } 297 | 298 | type blkioStatSettings struct { 299 | name string 300 | entry *[]*v1.BlkIOEntry 301 | } 302 | 303 | func uintf(v interface{}) []byte { 304 | return []byte(strconv.FormatUint(uint64(*v.(*uint16)), 10)) 305 | } 306 | 307 | func weightdev(v interface{}) []byte { 308 | wd := v.(specs.LinuxWeightDevice) 309 | return []byte(fmt.Sprintf("%d:%d %d", wd.Major, wd.Minor, *wd.Weight)) 310 | } 311 | 312 | func weightleafdev(v interface{}) []byte { 313 | wd := v.(specs.LinuxWeightDevice) 314 | return []byte(fmt.Sprintf("%d:%d %d", wd.Major, wd.Minor, *wd.LeafWeight)) 315 | } 316 | 317 | func throttleddev(v interface{}) []byte { 318 | td := v.(specs.LinuxThrottleDevice) 319 | return []byte(fmt.Sprintf("%d:%d %d", td.Major, td.Minor, td.Rate)) 320 | } 321 | 322 | func splitBlkIOStatLine(r rune) bool { 323 | return r == ' ' || r == ':' 324 | } 325 | 326 | type deviceKey struct { 327 | major, minor uint64 328 | } 329 | 330 | // getDevices makes a best effort attempt to read all the devices into a map 331 | // keyed by major and minor number. Since devices may be mapped multiple times, 332 | // we err on taking the first occurrence. 333 | func getDevices(r io.Reader) (map[deviceKey]string, error) { 334 | var ( 335 | s = bufio.NewScanner(r) 336 | devices = make(map[deviceKey]string) 337 | ) 338 | for i := 0; s.Scan(); i++ { 339 | if i < 2 { 340 | continue 341 | } 342 | fields := strings.Fields(s.Text()) 343 | major, err := strconv.Atoi(fields[0]) 344 | if err != nil { 345 | return nil, err 346 | } 347 | minor, err := strconv.Atoi(fields[1]) 348 | if err != nil { 349 | return nil, err 350 | } 351 | key := deviceKey{ 352 | major: uint64(major), 353 | minor: uint64(minor), 354 | } 355 | if _, ok := devices[key]; ok { 356 | continue 357 | } 358 | devices[key] = filepath.Join("/dev", fields[3]) 359 | } 360 | return devices, s.Err() 361 | } 362 | -------------------------------------------------------------------------------- /cgroup1/blkio_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "strings" 22 | "testing" 23 | 24 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 25 | ) 26 | 27 | const data = `major minor #blocks name 28 | 29 | 7 0 4 loop0 30 | 7 1 163456 loop1 31 | 7 2 149616 loop2 32 | 7 3 147684 loop3 33 | 7 4 122572 loop4 34 | 7 5 8936 loop5 35 | 7 6 31464 loop6 36 | 7 7 182432 loop7 37 | 259 0 937692504 nvme0n1 38 | 259 1 31744 nvme0n1p1 39 | ` 40 | 41 | func TestGetDevices(t *testing.T) { 42 | r := strings.NewReader(data) 43 | devices, err := getDevices(r) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | for dev, expected := range map[deviceKey]string{ 48 | {7, 0}: "/dev/loop0", 49 | {259, 0}: "/dev/nvme0n1", 50 | {259, 1}: "/dev/nvme0n1p1", 51 | } { 52 | name, ok := devices[dev] 53 | if !ok { 54 | t.Fatalf("no device found for %d:%d", dev.major, dev.minor) 55 | } 56 | if name != expected { 57 | t.Fatalf("expected device name %q but received %q", expected, name) 58 | } 59 | } 60 | } 61 | 62 | func TestNewBlkio(t *testing.T) { 63 | const root = "/test/folder" 64 | const expected = "/test/folder/blkio" 65 | const expectedProc = "/proc" 66 | 67 | ctrl := NewBlkio(root) 68 | if ctrl.root != expected { 69 | t.Fatalf("expected cgroups root %q but received %q", expected, ctrl.root) 70 | } 71 | if ctrl.procRoot != expectedProc { 72 | t.Fatalf("expected proc FS root %q but received %q", expectedProc, ctrl.procRoot) 73 | } 74 | } 75 | 76 | func TestBlkioStat(t *testing.T) { 77 | _, err := os.Stat("/sys/fs/cgroup/blkio") 78 | if os.IsNotExist(err) { 79 | t.Skip("failed to find /sys/fs/cgroup/blkio") 80 | } 81 | 82 | ctrl := NewBlkio("/sys/fs/cgroup") 83 | 84 | var metrics v1.Metrics 85 | err = ctrl.Stat("", &metrics) 86 | if err != nil { 87 | t.Fatalf("failed to call Stat: %v", err) 88 | } 89 | 90 | if len(metrics.Blkio.IoServicedRecursive) == 0 { 91 | t.Fatalf("IoServicedRecursive must not be empty") 92 | } 93 | if len(metrics.Blkio.IoServiceBytesRecursive) == 0 { 94 | t.Fatalf("IoServiceBytesRecursive must not be empty") 95 | } 96 | } 97 | 98 | func TestNewBlkio_Proc(t *testing.T) { 99 | const root = "/test/folder" 100 | const expected = "/test/folder/blkio" 101 | const expectedProc = "/test/proc" 102 | 103 | ctrl := NewBlkio(root, ProcRoot(expectedProc)) 104 | if ctrl.root != expected { 105 | t.Fatalf("expected cgroups root %q but received %q", expected, ctrl.root) 106 | } 107 | if ctrl.procRoot != expectedProc { 108 | t.Fatalf("expected proc FS root %q but received %q", expectedProc, ctrl.procRoot) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cgroup1/control.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | 22 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 23 | specs "github.com/opencontainers/runtime-spec/specs-go" 24 | ) 25 | 26 | type procType = string 27 | 28 | const ( 29 | cgroupProcs procType = "cgroup.procs" 30 | cgroupTasks procType = "tasks" 31 | defaultDirPerm = 0o755 32 | ) 33 | 34 | // defaultFilePerm is a var so that the test framework can change the filemode 35 | // of all files created when the tests are running. The difference between the 36 | // tests and real world use is that files like "cgroup.procs" will exist when writing 37 | // to a read cgroup filesystem and do not exist prior when running in the tests. 38 | // this is set to a non 0 value in the test code 39 | var defaultFilePerm = os.FileMode(0) 40 | 41 | type Process struct { 42 | // Subsystem is the name of the subsystem that the process / task is in. 43 | Subsystem Name 44 | // Pid is the process id of the process / task. 45 | Pid int 46 | // Path is the full path of the subsystem and location that the process / task is in. 47 | Path string 48 | } 49 | 50 | type Task = Process 51 | 52 | // Cgroup handles interactions with the individual groups to perform 53 | // actions on them as them main interface to this cgroup package 54 | type Cgroup interface { 55 | // New creates a new cgroup under the calling cgroup 56 | New(string, *specs.LinuxResources) (Cgroup, error) 57 | // Add adds a process to the cgroup (cgroup.procs). Without additional arguments, 58 | // the process is added to all the cgroup subsystems. When giving Add a list of 59 | // subsystem names, the process is only added to those subsystems, provided that 60 | // they are active in the targeted cgroup. 61 | Add(Process, ...Name) error 62 | // AddProc adds the process with the given id to the cgroup (cgroup.procs). 63 | // Without additional arguments, the process with the given id is added to all 64 | // the cgroup subsystems. When giving AddProc a list of subsystem names, the process 65 | // id is only added to those subsystems, provided that they are active in the targeted 66 | // cgroup. 67 | AddProc(uint64, ...Name) error 68 | // AddTask adds a process to the cgroup (tasks). Without additional arguments, the 69 | // task is added to all the cgroup subsystems. When giving AddTask a list of subsystem 70 | // names, the task is only added to those subsystems, provided that they are active in 71 | // the targeted cgroup. 72 | AddTask(Process, ...Name) error 73 | // Delete removes the cgroup as a whole 74 | Delete() error 75 | // MoveTo moves all the processes under the calling cgroup to the provided one 76 | // subsystems are moved one at a time 77 | MoveTo(Cgroup) error 78 | // Stat returns the stats for all subsystems in the cgroup 79 | Stat(...ErrorHandler) (*v1.Metrics, error) 80 | // Update updates all the subsystems with the provided resource changes 81 | Update(resources *specs.LinuxResources) error 82 | // Processes returns all the processes in a select subsystem for the cgroup 83 | Processes(Name, bool) ([]Process, error) 84 | // Tasks returns all the tasks in a select subsystem for the cgroup 85 | Tasks(Name, bool) ([]Task, error) 86 | // Freeze freezes or pauses all processes inside the cgroup 87 | Freeze() error 88 | // Thaw thaw or resumes all processes inside the cgroup 89 | Thaw() error 90 | // OOMEventFD returns the memory subsystem's event fd for OOM events 91 | OOMEventFD() (uintptr, error) 92 | // RegisterMemoryEvent returns the memory subsystems event fd for whatever memory event was 93 | // registered for. Can alternatively register for the oom event with this method. 94 | RegisterMemoryEvent(MemoryEvent) (uintptr, error) 95 | // State returns the cgroups current state 96 | State() State 97 | // Subsystems returns all the subsystems in the cgroup 98 | Subsystems() []Subsystem 99 | } 100 | -------------------------------------------------------------------------------- /cgroup1/cpu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bufio" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | 25 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 26 | specs "github.com/opencontainers/runtime-spec/specs-go" 27 | ) 28 | 29 | func NewCpu(root string) *cpuController { 30 | return &cpuController{ 31 | root: filepath.Join(root, string(Cpu)), 32 | } 33 | } 34 | 35 | type cpuController struct { 36 | root string 37 | } 38 | 39 | func (c *cpuController) Name() Name { 40 | return Cpu 41 | } 42 | 43 | func (c *cpuController) Path(path string) string { 44 | return filepath.Join(c.root, path) 45 | } 46 | 47 | func (c *cpuController) Create(path string, resources *specs.LinuxResources) error { 48 | if err := os.MkdirAll(c.Path(path), defaultDirPerm); err != nil { 49 | return err 50 | } 51 | if cpu := resources.CPU; cpu != nil { 52 | for _, t := range []struct { 53 | name string 54 | ivalue *int64 55 | uvalue *uint64 56 | }{ 57 | { 58 | name: "rt_period_us", 59 | uvalue: cpu.RealtimePeriod, 60 | }, 61 | { 62 | name: "rt_runtime_us", 63 | ivalue: cpu.RealtimeRuntime, 64 | }, 65 | { 66 | name: "shares", 67 | uvalue: cpu.Shares, 68 | }, 69 | { 70 | name: "cfs_period_us", 71 | uvalue: cpu.Period, 72 | }, 73 | { 74 | name: "cfs_quota_us", 75 | ivalue: cpu.Quota, 76 | }, 77 | } { 78 | var value []byte 79 | if t.uvalue != nil { 80 | value = []byte(strconv.FormatUint(*t.uvalue, 10)) 81 | } else if t.ivalue != nil { 82 | value = []byte(strconv.FormatInt(*t.ivalue, 10)) 83 | } 84 | if value != nil { 85 | if err := os.WriteFile( 86 | filepath.Join(c.Path(path), "cpu."+t.name), 87 | value, 88 | defaultFilePerm, 89 | ); err != nil { 90 | return err 91 | } 92 | } 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func (c *cpuController) Update(path string, resources *specs.LinuxResources) error { 99 | return c.Create(path, resources) 100 | } 101 | 102 | func (c *cpuController) Stat(path string, stats *v1.Metrics) error { 103 | f, err := os.Open(filepath.Join(c.Path(path), "cpu.stat")) 104 | if err != nil { 105 | return err 106 | } 107 | defer f.Close() 108 | // get or create the cpu field because cpuacct can also set values on this struct 109 | sc := bufio.NewScanner(f) 110 | for sc.Scan() { 111 | key, v, err := parseKV(sc.Text()) 112 | if err != nil { 113 | return err 114 | } 115 | switch key { 116 | case "nr_periods": 117 | stats.CPU.Throttling.Periods = v 118 | case "nr_throttled": 119 | stats.CPU.Throttling.ThrottledPeriods = v 120 | case "throttled_time": 121 | stats.CPU.Throttling.ThrottledTime = v 122 | } 123 | } 124 | return sc.Err() 125 | } 126 | -------------------------------------------------------------------------------- /cgroup1/cpuacct.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bufio" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strconv" 25 | "strings" 26 | 27 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 28 | ) 29 | 30 | const nanosecondsInSecond = 1000000000 31 | 32 | var clockTicks = getClockTicks() 33 | 34 | func NewCpuacct(root string) *cpuacctController { 35 | return &cpuacctController{ 36 | root: filepath.Join(root, string(Cpuacct)), 37 | } 38 | } 39 | 40 | type cpuacctController struct { 41 | root string 42 | } 43 | 44 | func (c *cpuacctController) Name() Name { 45 | return Cpuacct 46 | } 47 | 48 | func (c *cpuacctController) Path(path string) string { 49 | return filepath.Join(c.root, path) 50 | } 51 | 52 | func (c *cpuacctController) Stat(path string, stats *v1.Metrics) error { 53 | user, kernel, err := c.getUsage(path) 54 | if err != nil { 55 | return err 56 | } 57 | total, err := readUint(filepath.Join(c.Path(path), "cpuacct.usage")) 58 | if err != nil { 59 | return err 60 | } 61 | percpu, err := c.percpuUsage(path) 62 | if err != nil { 63 | return err 64 | } 65 | stats.CPU.Usage.Total = total 66 | stats.CPU.Usage.User = user 67 | stats.CPU.Usage.Kernel = kernel 68 | stats.CPU.Usage.PerCPU = percpu 69 | return nil 70 | } 71 | 72 | func (c *cpuacctController) percpuUsage(path string) ([]uint64, error) { 73 | var usage []uint64 74 | data, err := os.ReadFile(filepath.Join(c.Path(path), "cpuacct.usage_percpu")) 75 | if err != nil { 76 | return nil, err 77 | } 78 | for _, v := range strings.Fields(string(data)) { 79 | u, err := strconv.ParseUint(v, 10, 64) 80 | if err != nil { 81 | return nil, err 82 | } 83 | usage = append(usage, u) 84 | } 85 | return usage, nil 86 | } 87 | 88 | func (c *cpuacctController) getUsage(path string) (user uint64, kernel uint64, err error) { 89 | statPath := filepath.Join(c.Path(path), "cpuacct.stat") 90 | f, err := os.Open(statPath) 91 | if err != nil { 92 | return 0, 0, err 93 | } 94 | defer f.Close() 95 | var ( 96 | raw = make(map[string]uint64) 97 | sc = bufio.NewScanner(f) 98 | ) 99 | for sc.Scan() { 100 | key, v, err := parseKV(sc.Text()) 101 | if err != nil { 102 | return 0, 0, err 103 | } 104 | raw[key] = v 105 | } 106 | if err := sc.Err(); err != nil { 107 | return 0, 0, err 108 | } 109 | for _, t := range []struct { 110 | name string 111 | value *uint64 112 | }{ 113 | { 114 | name: "user", 115 | value: &user, 116 | }, 117 | { 118 | name: "system", 119 | value: &kernel, 120 | }, 121 | } { 122 | v, ok := raw[t.name] 123 | if !ok { 124 | return 0, 0, fmt.Errorf("expected field %q but not found in %q", t.name, statPath) 125 | } 126 | *t.value = v 127 | } 128 | return (user * nanosecondsInSecond) / clockTicks, (kernel * nanosecondsInSecond) / clockTicks, nil 129 | } 130 | -------------------------------------------------------------------------------- /cgroup1/cpuacct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | ) 24 | 25 | const cpuacctStatData = `user 1 26 | system 2 27 | sched_delay 3 28 | ` 29 | 30 | func TestGetUsage(t *testing.T) { 31 | mock, err := newMock(t) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer func() { 36 | if err := mock.delete(); err != nil { 37 | t.Errorf("failed delete: %v", err) 38 | } 39 | }() 40 | cpuacct := NewCpuacct(mock.root) 41 | if cpuacct == nil { 42 | t.Fatal("cpuacct is nil") 43 | } 44 | err = os.Mkdir(filepath.Join(mock.root, string(Cpuacct), "test"), defaultDirPerm) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | current := filepath.Join(mock.root, string(Cpuacct), "test", "cpuacct.stat") 49 | if err = os.WriteFile( 50 | current, 51 | []byte(cpuacctStatData), 52 | defaultFilePerm, 53 | ); err != nil { 54 | t.Fatal(err) 55 | } 56 | user, kernel, err := cpuacct.getUsage("test") 57 | if err != nil { 58 | t.Fatalf("can't get usage: %v", err) 59 | } 60 | index := []uint64{ 61 | user, 62 | kernel, 63 | } 64 | for i, v := range index { 65 | expected := ((uint64(i) + 1) * nanosecondsInSecond) / clockTicks 66 | if v != expected { 67 | t.Errorf("expected value at index %d to be %d but received %d", i, expected, v) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cgroup1/cpuset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | specs "github.com/opencontainers/runtime-spec/specs-go" 26 | ) 27 | 28 | func NewCpuset(root string) *cpusetController { 29 | return &cpusetController{ 30 | root: filepath.Join(root, string(Cpuset)), 31 | } 32 | } 33 | 34 | type cpusetController struct { 35 | root string 36 | } 37 | 38 | func (c *cpusetController) Name() Name { 39 | return Cpuset 40 | } 41 | 42 | func (c *cpusetController) Path(path string) string { 43 | return filepath.Join(c.root, path) 44 | } 45 | 46 | func (c *cpusetController) Create(path string, resources *specs.LinuxResources) error { 47 | if err := c.ensureParent(c.Path(path), c.root); err != nil { 48 | return err 49 | } 50 | if err := os.MkdirAll(c.Path(path), defaultDirPerm); err != nil { 51 | return err 52 | } 53 | if err := c.copyIfNeeded(c.Path(path), filepath.Dir(c.Path(path))); err != nil { 54 | return err 55 | } 56 | if resources.CPU != nil { 57 | for _, t := range []struct { 58 | name string 59 | value string 60 | }{ 61 | { 62 | name: "cpus", 63 | value: resources.CPU.Cpus, 64 | }, 65 | { 66 | name: "mems", 67 | value: resources.CPU.Mems, 68 | }, 69 | } { 70 | if t.value != "" { 71 | if err := os.WriteFile( 72 | filepath.Join(c.Path(path), "cpuset."+t.name), 73 | []byte(t.value), 74 | defaultFilePerm, 75 | ); err != nil { 76 | return err 77 | } 78 | } 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func (c *cpusetController) Update(path string, resources *specs.LinuxResources) error { 85 | return c.Create(path, resources) 86 | } 87 | 88 | func (c *cpusetController) getValues(path string) (cpus []byte, mems []byte, err error) { 89 | cpus, err = os.ReadFile(filepath.Join(path, "cpuset.cpus")) 90 | if err != nil && !os.IsNotExist(err) { 91 | return nil, nil, err 92 | } 93 | mems, err = os.ReadFile(filepath.Join(path, "cpuset.mems")) 94 | if err != nil && !os.IsNotExist(err) { 95 | return nil, nil, err 96 | } 97 | return cpus, mems, nil 98 | } 99 | 100 | // ensureParent makes sure that the parent directory of current is created 101 | // and populated with the proper cpus and mems files copied from 102 | // it's parent. 103 | func (c *cpusetController) ensureParent(current, root string) error { 104 | parent := filepath.Dir(current) 105 | if _, err := filepath.Rel(root, parent); err != nil { 106 | return nil 107 | } 108 | // Avoid infinite recursion. 109 | if parent == current { 110 | return fmt.Errorf("cpuset: cgroup parent path outside cgroup root") 111 | } 112 | if cleanPath(parent) != root { 113 | if err := c.ensureParent(parent, root); err != nil { 114 | return err 115 | } 116 | } 117 | if err := os.MkdirAll(current, defaultDirPerm); err != nil { 118 | return err 119 | } 120 | return c.copyIfNeeded(current, parent) 121 | } 122 | 123 | // copyIfNeeded copies the cpuset.cpus and cpuset.mems from the parent 124 | // directory to the current directory if the file's contents are 0 125 | func (c *cpusetController) copyIfNeeded(current, parent string) error { 126 | var ( 127 | err error 128 | currentCpus, currentMems []byte 129 | parentCpus, parentMems []byte 130 | ) 131 | if currentCpus, currentMems, err = c.getValues(current); err != nil { 132 | return err 133 | } 134 | if parentCpus, parentMems, err = c.getValues(parent); err != nil { 135 | return err 136 | } 137 | if isEmpty(currentCpus) { 138 | if err := os.WriteFile( 139 | filepath.Join(current, "cpuset.cpus"), 140 | parentCpus, 141 | defaultFilePerm, 142 | ); err != nil { 143 | return err 144 | } 145 | } 146 | if isEmpty(currentMems) { 147 | if err := os.WriteFile( 148 | filepath.Join(current, "cpuset.mems"), 149 | parentMems, 150 | defaultFilePerm, 151 | ); err != nil { 152 | return err 153 | } 154 | } 155 | return nil 156 | } 157 | 158 | func isEmpty(b []byte) bool { 159 | return len(bytes.Trim(b, "\n")) == 0 160 | } 161 | -------------------------------------------------------------------------------- /cgroup1/devices.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | specs "github.com/opencontainers/runtime-spec/specs-go" 25 | ) 26 | 27 | const ( 28 | allowDeviceFile = "devices.allow" 29 | denyDeviceFile = "devices.deny" 30 | wildcard = -1 31 | ) 32 | 33 | func NewDevices(root string) *devicesController { 34 | return &devicesController{ 35 | root: filepath.Join(root, string(Devices)), 36 | } 37 | } 38 | 39 | type devicesController struct { 40 | root string 41 | } 42 | 43 | func (d *devicesController) Name() Name { 44 | return Devices 45 | } 46 | 47 | func (d *devicesController) Path(path string) string { 48 | return filepath.Join(d.root, path) 49 | } 50 | 51 | func (d *devicesController) Create(path string, resources *specs.LinuxResources) error { 52 | if err := os.MkdirAll(d.Path(path), defaultDirPerm); err != nil { 53 | return err 54 | } 55 | for _, device := range resources.Devices { 56 | file := denyDeviceFile 57 | if device.Allow { 58 | file = allowDeviceFile 59 | } 60 | if device.Type == "" { 61 | device.Type = "a" 62 | } 63 | if err := os.WriteFile( 64 | filepath.Join(d.Path(path), file), 65 | []byte(deviceString(device)), 66 | defaultFilePerm, 67 | ); err != nil { 68 | return err 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func (d *devicesController) Update(path string, resources *specs.LinuxResources) error { 75 | return d.Create(path, resources) 76 | } 77 | 78 | func deviceString(device specs.LinuxDeviceCgroup) string { 79 | return fmt.Sprintf("%s %s:%s %s", 80 | device.Type, 81 | deviceNumber(device.Major), 82 | deviceNumber(device.Minor), 83 | device.Access, 84 | ) 85 | } 86 | 87 | func deviceNumber(number *int64) string { 88 | if number == nil || *number == wildcard { 89 | return "*" 90 | } 91 | return fmt.Sprint(*number) 92 | } 93 | -------------------------------------------------------------------------------- /cgroup1/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "errors" 21 | "os" 22 | ) 23 | 24 | var ( 25 | ErrInvalidPid = errors.New("cgroups: pid must be greater than 0") 26 | ErrMountPointNotExist = errors.New("cgroups: cgroup mountpoint does not exist") 27 | ErrInvalidFormat = errors.New("cgroups: parsing file with invalid format failed") 28 | ErrFreezerNotSupported = errors.New("cgroups: freezer cgroup not supported on this system") 29 | ErrMemoryNotSupported = errors.New("cgroups: memory cgroup not supported on this system") 30 | ErrCgroupDeleted = errors.New("cgroups: cgroup deleted") 31 | ErrNoCgroupMountDestination = errors.New("cgroups: cannot find cgroup mount destination") 32 | ) 33 | 34 | // ErrorHandler is a function that handles and acts on errors 35 | type ErrorHandler func(err error) error 36 | 37 | // IgnoreNotExist ignores any errors that are for not existing files 38 | func IgnoreNotExist(err error) error { 39 | if os.IsNotExist(err) { 40 | return nil 41 | } 42 | return err 43 | } 44 | 45 | func errPassthrough(err error) error { 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /cgroup1/freezer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | func NewFreezer(root string) *freezerController { 27 | return &freezerController{ 28 | root: filepath.Join(root, string(Freezer)), 29 | } 30 | } 31 | 32 | type freezerController struct { 33 | root string 34 | } 35 | 36 | func (f *freezerController) Name() Name { 37 | return Freezer 38 | } 39 | 40 | func (f *freezerController) Path(path string) string { 41 | return filepath.Join(f.root, path) 42 | } 43 | 44 | func (f *freezerController) Freeze(path string) error { 45 | return f.waitState(path, Frozen) 46 | } 47 | 48 | func (f *freezerController) Thaw(path string) error { 49 | return f.waitState(path, Thawed) 50 | } 51 | 52 | func (f *freezerController) changeState(path string, state State) error { 53 | return os.WriteFile( 54 | filepath.Join(f.root, path, "freezer.state"), 55 | []byte(strings.ToUpper(string(state))), 56 | defaultFilePerm, 57 | ) 58 | } 59 | 60 | func (f *freezerController) state(path string) (State, error) { 61 | current, err := os.ReadFile(filepath.Join(f.root, path, "freezer.state")) 62 | if err != nil { 63 | return "", err 64 | } 65 | return State(strings.ToLower(strings.TrimSpace(string(current)))), nil 66 | } 67 | 68 | func (f *freezerController) waitState(path string, state State) error { 69 | for { 70 | if err := f.changeState(path, state); err != nil { 71 | return err 72 | } 73 | current, err := f.state(path) 74 | if err != nil { 75 | return err 76 | } 77 | if current == state { 78 | return nil 79 | } 80 | time.Sleep(1 * time.Millisecond) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cgroup1/hierarchy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | // Hierarchy enables both unified and split hierarchy for cgroups 20 | type Hierarchy func() ([]Subsystem, error) 21 | -------------------------------------------------------------------------------- /cgroup1/hugetlb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strconv" 23 | "strings" 24 | 25 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 26 | specs "github.com/opencontainers/runtime-spec/specs-go" 27 | ) 28 | 29 | func NewHugetlb(root string) (*hugetlbController, error) { 30 | sizes, err := hugePageSizes() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &hugetlbController{ 36 | root: filepath.Join(root, string(Hugetlb)), 37 | sizes: sizes, 38 | }, nil 39 | } 40 | 41 | type hugetlbController struct { 42 | root string 43 | sizes []string 44 | } 45 | 46 | func (h *hugetlbController) Name() Name { 47 | return Hugetlb 48 | } 49 | 50 | func (h *hugetlbController) Path(path string) string { 51 | return filepath.Join(h.root, path) 52 | } 53 | 54 | func (h *hugetlbController) Create(path string, resources *specs.LinuxResources) error { 55 | if err := os.MkdirAll(h.Path(path), defaultDirPerm); err != nil { 56 | return err 57 | } 58 | for _, limit := range resources.HugepageLimits { 59 | if err := os.WriteFile( 60 | filepath.Join(h.Path(path), strings.Join([]string{"hugetlb", limit.Pagesize, "limit_in_bytes"}, ".")), 61 | []byte(strconv.FormatUint(limit.Limit, 10)), 62 | defaultFilePerm, 63 | ); err != nil { 64 | return err 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func (h *hugetlbController) Stat(path string, stats *v1.Metrics) error { 71 | for _, size := range h.sizes { 72 | s, err := h.readSizeStat(path, size) 73 | if err != nil { 74 | return err 75 | } 76 | stats.Hugetlb = append(stats.Hugetlb, s) 77 | } 78 | return nil 79 | } 80 | 81 | func (h *hugetlbController) readSizeStat(path, size string) (*v1.HugetlbStat, error) { 82 | s := v1.HugetlbStat{ 83 | Pagesize: size, 84 | } 85 | for _, t := range []struct { 86 | name string 87 | value *uint64 88 | }{ 89 | { 90 | name: "usage_in_bytes", 91 | value: &s.Usage, 92 | }, 93 | { 94 | name: "max_usage_in_bytes", 95 | value: &s.Max, 96 | }, 97 | { 98 | name: "failcnt", 99 | value: &s.Failcnt, 100 | }, 101 | } { 102 | v, err := readUint(filepath.Join(h.Path(path), strings.Join([]string{"hugetlb", size, t.name}, "."))) 103 | if err != nil { 104 | return nil, err 105 | } 106 | *t.value = v 107 | } 108 | return &s, nil 109 | } 110 | -------------------------------------------------------------------------------- /cgroup1/memory_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path" 23 | "strings" 24 | "testing" 25 | 26 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 27 | specs "github.com/opencontainers/runtime-spec/specs-go" 28 | ) 29 | 30 | const memoryData = `cache 1 31 | rss 2 32 | rss_huge 3 33 | mapped_file 4 34 | dirty 5 35 | writeback 6 36 | pgpgin 7 37 | pgpgout 8 38 | pgfault 9 39 | pgmajfault 10 40 | inactive_anon 11 41 | active_anon 12 42 | inactive_file 13 43 | active_file 14 44 | unevictable 15 45 | hierarchical_memory_limit 16 46 | hierarchical_memsw_limit 17 47 | total_cache 18 48 | total_rss 19 49 | total_rss_huge 20 50 | total_mapped_file 21 51 | total_dirty 22 52 | total_writeback 23 53 | total_pgpgin 24 54 | total_pgpgout 25 55 | total_pgfault 26 56 | total_pgmajfault 27 57 | total_inactive_anon 28 58 | total_active_anon 29 59 | total_inactive_file 30 60 | total_active_file 31 61 | total_unevictable 32 62 | ` 63 | 64 | const memoryOomControlData = `oom_kill_disable 1 65 | under_oom 2 66 | oom_kill 3 67 | ` 68 | 69 | func TestParseMemoryStats(t *testing.T) { 70 | var ( 71 | c = &memoryController{} 72 | m = &v1.MemoryStat{} 73 | r = strings.NewReader(memoryData) 74 | ) 75 | if err := c.parseStats(r, m); err != nil { 76 | t.Fatal(err) 77 | } 78 | index := []uint64{ 79 | m.Cache, 80 | m.RSS, 81 | m.RSSHuge, 82 | m.MappedFile, 83 | m.Dirty, 84 | m.Writeback, 85 | m.PgPgIn, 86 | m.PgPgOut, 87 | m.PgFault, 88 | m.PgMajFault, 89 | m.InactiveAnon, 90 | m.ActiveAnon, 91 | m.InactiveFile, 92 | m.ActiveFile, 93 | m.Unevictable, 94 | m.HierarchicalMemoryLimit, 95 | m.HierarchicalSwapLimit, 96 | m.TotalCache, 97 | m.TotalRSS, 98 | m.TotalRSSHuge, 99 | m.TotalMappedFile, 100 | m.TotalDirty, 101 | m.TotalWriteback, 102 | m.TotalPgPgIn, 103 | m.TotalPgPgOut, 104 | m.TotalPgFault, 105 | m.TotalPgMajFault, 106 | m.TotalInactiveAnon, 107 | m.TotalActiveAnon, 108 | m.TotalInactiveFile, 109 | m.TotalActiveFile, 110 | m.TotalUnevictable, 111 | } 112 | for i, v := range index { 113 | if v != uint64(i)+1 { 114 | t.Errorf("expected value at index %d to be %d but received %d", i, i+1, v) 115 | } 116 | } 117 | } 118 | 119 | func TestParseMemoryOomControl(t *testing.T) { 120 | var ( 121 | c = &memoryController{} 122 | m = &v1.MemoryOomControl{} 123 | r = strings.NewReader(memoryOomControlData) 124 | ) 125 | if err := c.parseOomControlStats(r, m); err != nil { 126 | t.Fatal(err) 127 | } 128 | index := []uint64{ 129 | m.OomKillDisable, 130 | m.UnderOom, 131 | m.OomKill, 132 | } 133 | for i, v := range index { 134 | if v != uint64(i)+1 { 135 | t.Errorf("expected value at index %d to be %d but received %d", i, i+1, v) 136 | } 137 | } 138 | } 139 | 140 | func TestMemoryController_Stat(t *testing.T) { 141 | // GIVEN a cgroups folder with all the memory metrics 142 | modules := []string{"", "memsw", "kmem", "kmem.tcp"} 143 | metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"} 144 | tmpRoot := buildMemoryMetrics(t, modules, metrics) 145 | 146 | // WHEN the memory controller reads the metrics stats 147 | mc := NewMemory(tmpRoot) 148 | stats := v1.Metrics{} 149 | if err := mc.Stat("", &stats); err != nil { 150 | t.Errorf("can't get stats: %v", err) 151 | } 152 | 153 | // THEN all the memory stats have been completely loaded in memory 154 | checkMemoryStatIsComplete(t, stats.Memory) 155 | } 156 | 157 | func TestMemoryController_Stat_IgnoreModules(t *testing.T) { 158 | // GIVEN a cgroups folder that accounts for all the metrics BUT swap memory 159 | modules := []string{"", "kmem", "kmem.tcp"} 160 | metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"} 161 | tmpRoot := buildMemoryMetrics(t, modules, metrics) 162 | 163 | // WHEN the memory controller explicitly ignores memsw module and reads the data 164 | mc := NewMemory(tmpRoot, IgnoreModules("memsw")) 165 | stats := v1.Metrics{} 166 | if err := mc.Stat("", &stats); err != nil { 167 | t.Errorf("can't get stats: %v", err) 168 | } 169 | 170 | // THEN the swap memory stats are not loaded but all the other memory metrics are 171 | checkMemoryStatHasNoSwap(t, stats.Memory) 172 | } 173 | 174 | func TestMemoryController_Stat_OptionalSwap_HasSwap(t *testing.T) { 175 | // GIVEN a cgroups folder with all the memory metrics 176 | modules := []string{"", "memsw", "kmem", "kmem.tcp"} 177 | metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"} 178 | tmpRoot := buildMemoryMetrics(t, modules, metrics) 179 | 180 | // WHEN a memory controller that ignores swap only if it is missing reads stats 181 | mc := NewMemory(tmpRoot, OptionalSwap()) 182 | stats := v1.Metrics{} 183 | if err := mc.Stat("", &stats); err != nil { 184 | t.Errorf("can't get stats: %v", err) 185 | } 186 | 187 | // THEN all the memory stats have been completely loaded in memory 188 | checkMemoryStatIsComplete(t, stats.Memory) 189 | } 190 | 191 | func TestMemoryController_Stat_OptionalSwap_NoSwap(t *testing.T) { 192 | // GIVEN a cgroups folder that accounts for all the metrics BUT swap memory 193 | modules := []string{"", "kmem", "kmem.tcp"} 194 | metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"} 195 | tmpRoot := buildMemoryMetrics(t, modules, metrics) 196 | 197 | // WHEN a memory controller that ignores swap only if it is missing reads stats 198 | mc := NewMemory(tmpRoot, OptionalSwap()) 199 | stats := v1.Metrics{} 200 | if err := mc.Stat("", &stats); err != nil { 201 | t.Errorf("can't get stats: %v", err) 202 | } 203 | 204 | // THEN the swap memory stats are not loaded but all the other memory metrics are 205 | checkMemoryStatHasNoSwap(t, stats.Memory) 206 | } 207 | 208 | func checkMemoryStatIsComplete(t *testing.T, mem *v1.MemoryStat) { 209 | index := []uint64{ 210 | mem.Usage.Usage, 211 | mem.Usage.Max, 212 | mem.Usage.Failcnt, 213 | mem.Usage.Limit, 214 | mem.Swap.Usage, 215 | mem.Swap.Max, 216 | mem.Swap.Failcnt, 217 | mem.Swap.Limit, 218 | mem.Kernel.Usage, 219 | mem.Kernel.Max, 220 | mem.Kernel.Failcnt, 221 | mem.Kernel.Limit, 222 | mem.KernelTCP.Usage, 223 | mem.KernelTCP.Max, 224 | mem.KernelTCP.Failcnt, 225 | mem.KernelTCP.Limit, 226 | } 227 | for i, v := range index { 228 | if v != uint64(i) { 229 | t.Errorf("expected value at index %d to be %d but received %d", i, i, v) 230 | } 231 | } 232 | } 233 | 234 | func checkMemoryStatHasNoSwap(t *testing.T, mem *v1.MemoryStat) { 235 | if mem.Swap.Usage != 0 || mem.Swap.Limit != 0 || 236 | mem.Swap.Max != 0 || mem.Swap.Failcnt != 0 { 237 | t.Errorf("swap memory should have been ignored. Got: %+v", mem.Swap) 238 | } 239 | index := []uint64{ 240 | mem.Usage.Usage, 241 | mem.Usage.Max, 242 | mem.Usage.Failcnt, 243 | mem.Usage.Limit, 244 | mem.Kernel.Usage, 245 | mem.Kernel.Max, 246 | mem.Kernel.Failcnt, 247 | mem.Kernel.Limit, 248 | mem.KernelTCP.Usage, 249 | mem.KernelTCP.Max, 250 | mem.KernelTCP.Failcnt, 251 | mem.KernelTCP.Limit, 252 | } 253 | for i, v := range index { 254 | if v != uint64(i) { 255 | t.Errorf("expected value at index %d to be %d but received %d", i, i, v) 256 | } 257 | } 258 | } 259 | 260 | // buildMemoryMetrics creates fake cgroups memory entries in a temporary dir. Returns the fake cgroups root 261 | func buildMemoryMetrics(t *testing.T, modules []string, metrics []string) string { 262 | tmpRoot := t.TempDir() 263 | tmpDir := path.Join(tmpRoot, string(Memory)) 264 | if err := os.MkdirAll(tmpDir, defaultDirPerm); err != nil { 265 | t.Fatal(err) 266 | } 267 | if err := os.WriteFile(path.Join(tmpDir, "memory.stat"), []byte(memoryData), defaultFilePerm); err != nil { 268 | t.Fatal(err) 269 | } 270 | if err := os.WriteFile(path.Join(tmpDir, "memory.oom_control"), []byte(memoryOomControlData), defaultFilePerm); err != nil { 271 | t.Fatal(err) 272 | } 273 | cnt := 0 274 | for _, mod := range modules { 275 | for _, metric := range metrics { 276 | var fileName string 277 | if mod == "" { 278 | fileName = path.Join(tmpDir, strings.Join([]string{"memory", metric}, ".")) 279 | } else { 280 | fileName = path.Join(tmpDir, strings.Join([]string{"memory", mod, metric}, ".")) 281 | } 282 | if err := os.WriteFile(fileName, []byte(fmt.Sprintln(cnt)), defaultFilePerm); err != nil { 283 | t.Fatal(err) 284 | } 285 | cnt++ 286 | } 287 | } 288 | return tmpRoot 289 | } 290 | 291 | func Test_getOomControlValue(t *testing.T) { 292 | var ( 293 | oneInt64 int64 = 1 294 | zeroInt64 int64 = 0 295 | trueBool bool = true 296 | falseBool bool = false 297 | ) 298 | 299 | type args struct { 300 | mem *specs.LinuxMemory 301 | } 302 | tests := []struct { 303 | name string 304 | args args 305 | want *int64 306 | }{ 307 | { 308 | name: "enable", 309 | args: args{ 310 | mem: &specs.LinuxMemory{ 311 | DisableOOMKiller: &falseBool, 312 | }, 313 | }, 314 | want: &zeroInt64, 315 | }, 316 | { 317 | name: "disable", 318 | args: args{ 319 | mem: &specs.LinuxMemory{ 320 | DisableOOMKiller: &trueBool, 321 | }, 322 | }, 323 | want: &oneInt64, 324 | }, 325 | { 326 | name: "nil", 327 | args: args{ 328 | mem: &specs.LinuxMemory{}, 329 | }, 330 | want: nil, 331 | }, 332 | } 333 | for _, tt := range tests { 334 | t.Run(tt.name, func(t *testing.T) { 335 | got := getOomControlValue(tt.args.mem) 336 | if (got == nil || tt.want == nil) && got != tt.want { 337 | t.Errorf("getOomControlValue() = %v, want %v", got, tt.want) 338 | return 339 | } 340 | if !(got == nil || tt.want == nil) && *got != *tt.want { 341 | t.Errorf("getOomControlValue() = %v, want %v", got, tt.want) 342 | } 343 | }) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /cgroup1/mock_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | ) 24 | 25 | func init() { 26 | defaultFilePerm = 0o666 27 | } 28 | 29 | func newMock(tb testing.TB) (*mockCgroup, error) { 30 | root := tb.TempDir() 31 | subsystems, err := defaults(root) 32 | if err != nil { 33 | return nil, err 34 | } 35 | for _, s := range subsystems { 36 | if err := os.MkdirAll(filepath.Join(root, string(s.Name())), defaultDirPerm); err != nil { 37 | return nil, err 38 | } 39 | } 40 | // make cpuset root files 41 | for _, v := range []struct { 42 | name string 43 | value []byte 44 | }{ 45 | { 46 | name: "cpuset.cpus", 47 | value: []byte("0-3"), 48 | }, 49 | { 50 | name: "cpuset.mems", 51 | value: []byte("0-3"), 52 | }, 53 | } { 54 | if err := os.WriteFile(filepath.Join(root, "cpuset", v.name), v.value, defaultFilePerm); err != nil { 55 | return nil, err 56 | } 57 | } 58 | return &mockCgroup{ 59 | root: root, 60 | subsystems: subsystems, 61 | }, nil 62 | } 63 | 64 | type mockCgroup struct { 65 | root string 66 | subsystems []Subsystem 67 | } 68 | 69 | func (m *mockCgroup) delete() error { 70 | return os.RemoveAll(m.root) 71 | } 72 | 73 | func (m *mockCgroup) hierarchy() ([]Subsystem, error) { 74 | return m.subsystems, nil 75 | } 76 | 77 | // symLink() creates a symlink between net_cls and net_prio for testing 78 | // On certain Linux systems, there's a symlink from both net_cls and net_prio to "net_cls,net_prio" 79 | // Since we don't have a subsystem defined for "net_cls,net_prio", 80 | // we mock this behavior by creating a symlink directly between net_cls and net_prio 81 | func (m *mockCgroup) symLink() error { 82 | netCLS := filepath.Join(m.root, string(NetCLS)) 83 | netPrio := filepath.Join(m.root, string(NetPrio)) 84 | // remove netCLS before creating a symlink 85 | if err := os.RemoveAll(netCLS); err != nil { 86 | return err 87 | } 88 | // create symlink between net_cls and net_prio 89 | if err := os.Symlink(netPrio, netCLS); err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /cgroup1/named.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import "path/filepath" 20 | 21 | func NewNamed(root string, name Name) *namedController { 22 | return &namedController{ 23 | root: root, 24 | name: name, 25 | } 26 | } 27 | 28 | type namedController struct { 29 | root string 30 | name Name 31 | } 32 | 33 | func (n *namedController) Name() Name { 34 | return n.name 35 | } 36 | 37 | func (n *namedController) Path(path string) string { 38 | return filepath.Join(n.root, string(n.name), path) 39 | } 40 | -------------------------------------------------------------------------------- /cgroup1/named_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import "testing" 20 | 21 | func TestNamedNameValue(t *testing.T) { 22 | n := NewNamed("/sys/fs/cgroup", "systemd") 23 | if n.name != "systemd" { 24 | t.Fatalf("expected name %q to be systemd", n.name) 25 | } 26 | } 27 | 28 | func TestNamedPath(t *testing.T) { 29 | n := NewNamed("/sys/fs/cgroup", "systemd") 30 | path := n.Path("/test") 31 | if expected := "/sys/fs/cgroup/systemd/test"; path != expected { 32 | t.Fatalf("expected %q but received %q from named cgroup", expected, path) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cgroup1/net_cls.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strconv" 23 | 24 | specs "github.com/opencontainers/runtime-spec/specs-go" 25 | ) 26 | 27 | func NewNetCls(root string) *netclsController { 28 | return &netclsController{ 29 | root: filepath.Join(root, string(NetCLS)), 30 | } 31 | } 32 | 33 | type netclsController struct { 34 | root string 35 | } 36 | 37 | func (n *netclsController) Name() Name { 38 | return NetCLS 39 | } 40 | 41 | func (n *netclsController) Path(path string) string { 42 | return filepath.Join(n.root, path) 43 | } 44 | 45 | func (n *netclsController) Create(path string, resources *specs.LinuxResources) error { 46 | if err := os.MkdirAll(n.Path(path), defaultDirPerm); err != nil { 47 | return err 48 | } 49 | if resources.Network != nil && resources.Network.ClassID != nil && *resources.Network.ClassID > 0 { 50 | return os.WriteFile( 51 | filepath.Join(n.Path(path), "net_cls.classid"), 52 | []byte(strconv.FormatUint(uint64(*resources.Network.ClassID), 10)), 53 | defaultFilePerm, 54 | ) 55 | } 56 | return nil 57 | } 58 | 59 | func (n *netclsController) Update(path string, resources *specs.LinuxResources) error { 60 | return n.Create(path, resources) 61 | } 62 | -------------------------------------------------------------------------------- /cgroup1/net_prio.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | specs "github.com/opencontainers/runtime-spec/specs-go" 25 | ) 26 | 27 | func NewNetPrio(root string) *netprioController { 28 | return &netprioController{ 29 | root: filepath.Join(root, string(NetPrio)), 30 | } 31 | } 32 | 33 | type netprioController struct { 34 | root string 35 | } 36 | 37 | func (n *netprioController) Name() Name { 38 | return NetPrio 39 | } 40 | 41 | func (n *netprioController) Path(path string) string { 42 | return filepath.Join(n.root, path) 43 | } 44 | 45 | func (n *netprioController) Create(path string, resources *specs.LinuxResources) error { 46 | if err := os.MkdirAll(n.Path(path), defaultDirPerm); err != nil { 47 | return err 48 | } 49 | if resources.Network != nil { 50 | for _, prio := range resources.Network.Priorities { 51 | if err := os.WriteFile( 52 | filepath.Join(n.Path(path), "net_prio.ifpriomap"), 53 | formatPrio(prio.Name, prio.Priority), 54 | defaultFilePerm, 55 | ); err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func formatPrio(name string, prio uint32) []byte { 64 | return []byte(fmt.Sprintf("%s %d", name, prio)) 65 | } 66 | -------------------------------------------------------------------------------- /cgroup1/opts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "errors" 21 | ) 22 | 23 | var ( 24 | // ErrIgnoreSubsystem allows the specific subsystem to be skipped 25 | ErrIgnoreSubsystem = errors.New("skip subsystem") 26 | // ErrDevicesRequired is returned when the devices subsystem is required but 27 | // does not exist or is not active 28 | ErrDevicesRequired = errors.New("devices subsystem is required") 29 | ) 30 | 31 | // InitOpts allows configuration for the creation or loading of a cgroup 32 | type InitOpts func(*InitConfig) error 33 | 34 | // InitConfig provides configuration options for the creation 35 | // or loading of a cgroup and its subsystems 36 | type InitConfig struct { 37 | // InitCheck can be used to check initialization errors from the subsystem 38 | InitCheck InitCheck 39 | hierarchy Hierarchy 40 | } 41 | 42 | func newInitConfig() *InitConfig { 43 | return &InitConfig{ 44 | InitCheck: RequireDevices, 45 | hierarchy: Default, 46 | } 47 | } 48 | 49 | // InitCheck allows subsystems errors to be checked when initialized or loaded 50 | type InitCheck func(Subsystem, Path, error) error 51 | 52 | // AllowAny allows any subsystem errors to be skipped 53 | func AllowAny(_ Subsystem, _ Path, _ error) error { 54 | return ErrIgnoreSubsystem 55 | } 56 | 57 | // RequireDevices requires the device subsystem but no others 58 | func RequireDevices(s Subsystem, _ Path, _ error) error { 59 | if s.Name() == Devices { 60 | return ErrDevicesRequired 61 | } 62 | return ErrIgnoreSubsystem 63 | } 64 | 65 | // WithHierarchy sets a list of cgroup subsystems. 66 | // The default list is coming from /proc/self/mountinfo. 67 | func WithHierarchy(h Hierarchy) InitOpts { 68 | return func(c *InitConfig) error { 69 | c.hierarchy = h 70 | return nil 71 | } 72 | } 73 | 74 | // WithHiearchy sets a list of cgroup subsystems. It is just kept for backward 75 | // compatibility and will be removed in v4. 76 | // 77 | // Deprecated: use WithHierarchy instead. 78 | func WithHiearchy(h Hierarchy) InitOpts { 79 | return WithHierarchy(h) 80 | } 81 | -------------------------------------------------------------------------------- /cgroup1/paths.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "path/filepath" 23 | ) 24 | 25 | type Path func(subsystem Name) (string, error) 26 | 27 | func RootPath(subsystem Name) (string, error) { 28 | return "/", nil 29 | } 30 | 31 | // StaticPath returns a static path to use for all cgroups 32 | func StaticPath(path string) Path { 33 | return func(_ Name) (string, error) { 34 | return path, nil 35 | } 36 | } 37 | 38 | // NestedPath will nest the cgroups based on the calling processes cgroup 39 | // placing its child processes inside its own path 40 | func NestedPath(suffix string) Path { 41 | paths, err := ParseCgroupFile("/proc/self/cgroup") 42 | if err != nil { 43 | return errorPath(err) 44 | } 45 | return existingPath(paths, suffix) 46 | } 47 | 48 | // PidPath will return the correct cgroup paths for an existing process running inside a cgroup 49 | // This is commonly used for the Load function to restore an existing container 50 | func PidPath(pid int) Path { 51 | p := fmt.Sprintf("/proc/%d/cgroup", pid) 52 | paths, err := ParseCgroupFile(p) 53 | if err != nil { 54 | return errorPath(fmt.Errorf("parse cgroup file %s: %w", p, err)) 55 | } 56 | return existingPath(paths, "") 57 | } 58 | 59 | // ErrControllerNotActive is returned when a controller is not supported or enabled 60 | var ErrControllerNotActive = errors.New("controller is not supported") 61 | 62 | func existingPath(paths map[string]string, suffix string) Path { 63 | // localize the paths based on the root mount dest for nested cgroups 64 | for n, p := range paths { 65 | dest, err := getCgroupDestination(n) 66 | if err != nil { 67 | return errorPath(err) 68 | } 69 | rel, err := filepath.Rel(dest, p) 70 | if err != nil { 71 | return errorPath(err) 72 | } 73 | if rel == "." { 74 | rel = dest 75 | } 76 | paths[n] = filepath.Join("/", rel) 77 | } 78 | return func(name Name) (string, error) { 79 | root, ok := paths[string(name)] 80 | if !ok { 81 | if root, ok = paths["name="+string(name)]; !ok { 82 | return "", ErrControllerNotActive 83 | } 84 | } 85 | if suffix != "" { 86 | return filepath.Join(root, suffix), nil 87 | } 88 | return root, nil 89 | } 90 | } 91 | 92 | func subPath(path Path, subName string) Path { 93 | return func(name Name) (string, error) { 94 | p, err := path(name) 95 | if err != nil { 96 | return "", err 97 | } 98 | return filepath.Join(p, subName), nil 99 | } 100 | } 101 | 102 | func errorPath(err error) Path { 103 | return func(_ Name) (string, error) { 104 | return "", err 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cgroup1/paths_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/containerd/cgroups/v3" 26 | ) 27 | 28 | func TestStaticPath(t *testing.T) { 29 | path := StaticPath("test") 30 | p, err := path("") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if p != "test" { 35 | t.Fatalf("expected static path of \"test\" but received %q", p) 36 | } 37 | } 38 | 39 | func TestSelfPath(t *testing.T) { 40 | _, err := v1MountPoint() 41 | if err == ErrMountPointNotExist { 42 | t.Skip("skipping test that requires cgroup hierarchy") 43 | } else if err != nil { 44 | t.Fatal(err) 45 | } 46 | paths, err := ParseCgroupFile("/proc/self/cgroup") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | dp := strings.TrimPrefix(paths["devices"], "/") 51 | path := NestedPath("test") 52 | p, err := path("devices") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if p != filepath.Join("/", dp, "test") { 57 | t.Fatalf("expected self path of %q but received %q", filepath.Join("/", dp, "test"), p) 58 | } 59 | } 60 | 61 | func TestPidPath(t *testing.T) { 62 | _, err := v1MountPoint() 63 | if err == ErrMountPointNotExist { 64 | t.Skip("skipping test that requires cgroup hierarchy") 65 | } else if err != nil { 66 | t.Fatal(err) 67 | } 68 | paths, err := ParseCgroupFile("/proc/self/cgroup") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | dp := strings.TrimPrefix(paths["devices"], "/") 73 | path := PidPath(os.Getpid()) 74 | p, err := path("devices") 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | if p != filepath.Join("/", dp) { 79 | t.Fatalf("expected self path of %q but received %q", filepath.Join("/", dp), p) 80 | } 81 | } 82 | 83 | func TestRootPath(t *testing.T) { 84 | p, err := RootPath(Cpu) 85 | if err != nil { 86 | t.Error(err) 87 | return 88 | } 89 | if p != "/" { 90 | t.Errorf("expected / but received %q", p) 91 | } 92 | } 93 | 94 | func TestSystemd240(t *testing.T) { 95 | if isUnified { 96 | t.Skipf("requires the system to be running in legacy mode") 97 | } 98 | const data = `8:net_cls:/ 99 | 7:memory:/system.slice/docker.service 100 | 6:freezer:/ 101 | 5:blkio:/system.slice/docker.service 102 | 4:devices:/system.slice/docker.service 103 | 3:cpuset:/ 104 | 2:cpu,cpuacct:/system.slice/docker.service 105 | 1:name=systemd:/system.slice/docker.service 106 | 0::/system.slice/docker.service` 107 | r := strings.NewReader(data) 108 | paths, unified, err := cgroups.ParseCgroupFromReaderUnified(r) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | path := existingPath(paths, "") 114 | _, err = path("net_prio") 115 | if err == nil { 116 | t.Fatal("error for net_prio should not be nil") 117 | } 118 | if err != ErrControllerNotActive { 119 | t.Fatalf("expected error %q but received %q", ErrControllerNotActive, err) 120 | } 121 | unifiedExpected := "/system.slice/docker.service" 122 | if unified != unifiedExpected { 123 | t.Fatalf("expected %q, got %q", unifiedExpected, unified) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /cgroup1/perf_event.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import "path/filepath" 20 | 21 | func NewPerfEvent(root string) *PerfEventController { 22 | return &PerfEventController{ 23 | root: filepath.Join(root, string(PerfEvent)), 24 | } 25 | } 26 | 27 | type PerfEventController struct { 28 | root string 29 | } 30 | 31 | func (p *PerfEventController) Name() Name { 32 | return PerfEvent 33 | } 34 | 35 | func (p *PerfEventController) Path(path string) string { 36 | return filepath.Join(p.root, path) 37 | } 38 | -------------------------------------------------------------------------------- /cgroup1/pids.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strconv" 23 | 24 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 25 | specs "github.com/opencontainers/runtime-spec/specs-go" 26 | ) 27 | 28 | func NewPids(root string) *pidsController { 29 | return &pidsController{ 30 | root: filepath.Join(root, string(Pids)), 31 | } 32 | } 33 | 34 | type pidsController struct { 35 | root string 36 | } 37 | 38 | func (p *pidsController) Name() Name { 39 | return Pids 40 | } 41 | 42 | func (p *pidsController) Path(path string) string { 43 | return filepath.Join(p.root, path) 44 | } 45 | 46 | func (p *pidsController) Create(path string, resources *specs.LinuxResources) error { 47 | if err := os.MkdirAll(p.Path(path), defaultDirPerm); err != nil { 48 | return err 49 | } 50 | if resources.Pids != nil && resources.Pids.Limit > 0 { 51 | return os.WriteFile( 52 | filepath.Join(p.Path(path), "pids.max"), 53 | []byte(strconv.FormatInt(resources.Pids.Limit, 10)), 54 | defaultFilePerm, 55 | ) 56 | } 57 | return nil 58 | } 59 | 60 | func (p *pidsController) Update(path string, resources *specs.LinuxResources) error { 61 | return p.Create(path, resources) 62 | } 63 | 64 | func (p *pidsController) Stat(path string, stats *v1.Metrics) error { 65 | current, err := readUint(filepath.Join(p.Path(path), "pids.current")) 66 | if err != nil { 67 | return err 68 | } 69 | pidsMax, err := readUint(filepath.Join(p.Path(path), "pids.max")) 70 | if err != nil { 71 | return err 72 | } 73 | stats.Pids = &v1.PidsStat{ 74 | Current: current, 75 | Limit: pidsMax, 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /cgroup1/pids_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "encoding/hex" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "testing" 25 | 26 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 27 | "github.com/opencontainers/runtime-spec/specs-go" 28 | ) 29 | 30 | func TestPids(t *testing.T) { 31 | mock, err := newMock(t) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer func() { 36 | if err := mock.delete(); err != nil { 37 | t.Errorf("failed delete: %v", err) 38 | } 39 | }() 40 | pids := NewPids(mock.root) 41 | if pids == nil { 42 | t.Fatal("pids is nil") 43 | } 44 | resources := specs.LinuxResources{} 45 | resources.Pids = &specs.LinuxPids{} 46 | resources.Pids.Limit = int64(10) 47 | err = pids.Create("test", &resources) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | current := filepath.Join(mock.root, "pids", "test", "pids.current") 52 | if err = os.WriteFile( 53 | current, 54 | []byte(strconv.Itoa(5)), 55 | defaultFilePerm, 56 | ); err != nil { 57 | t.Fatal(err) 58 | } 59 | metrics := v1.Metrics{} 60 | err = pids.Stat("test", &metrics) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if metrics.Pids.Limit != uint64(10) { 65 | t.Fatalf("expected pids limit %q but received %q", 66 | uint64(10), metrics.Pids.Limit) 67 | } 68 | if metrics.Pids.Current != uint64(5) { 69 | t.Fatalf("expected pids limit %q but received %q", 70 | uint64(5), metrics.Pids.Current) 71 | } 72 | resources.Pids.Limit = int64(15) 73 | err = pids.Update("test", &resources) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | err = pids.Stat("test", &metrics) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | if metrics.Pids.Limit != uint64(15) { 82 | t.Fatalf("expected pids limit %q but received %q", 83 | uint64(15), metrics.Pids.Limit) 84 | } 85 | if metrics.Pids.Current != uint64(5) { 86 | t.Fatalf("expected pids limit %q but received %q", 87 | uint64(5), metrics.Pids.Current) 88 | } 89 | } 90 | 91 | func TestPidsMissingCurrent(t *testing.T) { 92 | mock, err := newMock(t) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | defer func() { 97 | if err := mock.delete(); err != nil { 98 | t.Errorf("failed delete: %v", err) 99 | } 100 | }() 101 | pids := NewPids(mock.root) 102 | if pids == nil { 103 | t.Fatal("pids is nil") 104 | } 105 | metrics := v1.Metrics{} 106 | err = pids.Stat("test", &metrics) 107 | if err == nil { 108 | t.Fatal("expected not nil err") 109 | } 110 | } 111 | 112 | func TestPidsMissingMax(t *testing.T) { 113 | mock, err := newMock(t) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | defer func() { 118 | if err := mock.delete(); err != nil { 119 | t.Errorf("failed delete: %v", err) 120 | } 121 | }() 122 | pids := NewPids(mock.root) 123 | if pids == nil { 124 | t.Fatal("pids is nil") 125 | } 126 | err = os.Mkdir(filepath.Join(mock.root, "pids", "test"), defaultDirPerm) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | current := filepath.Join(mock.root, "pids", "test", "pids.current") 131 | if err = os.WriteFile( 132 | current, 133 | []byte(strconv.Itoa(5)), 134 | defaultFilePerm, 135 | ); err != nil { 136 | t.Fatal(err) 137 | } 138 | metrics := v1.Metrics{} 139 | err = pids.Stat("test", &metrics) 140 | if err == nil { 141 | t.Fatal("expected not nil err") 142 | } 143 | } 144 | 145 | func TestPidsOverflowMax(t *testing.T) { 146 | mock, err := newMock(t) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | defer func() { 151 | if err := mock.delete(); err != nil { 152 | t.Errorf("failed delete: %v", err) 153 | } 154 | }() 155 | pids := NewPids(mock.root) 156 | if pids == nil { 157 | t.Fatal("pids is nil") 158 | } 159 | err = os.Mkdir(filepath.Join(mock.root, "pids", "test"), defaultDirPerm) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | current := filepath.Join(mock.root, "pids", "test", "pids.current") 164 | if err = os.WriteFile( 165 | current, 166 | []byte(strconv.Itoa(5)), 167 | defaultFilePerm, 168 | ); err != nil { 169 | t.Fatal(err) 170 | } 171 | max := filepath.Join(mock.root, "pids", "test", "pids.max") 172 | bytes, err := hex.DecodeString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | if err = os.WriteFile( 177 | max, 178 | bytes, 179 | defaultFilePerm, 180 | ); err != nil { 181 | t.Fatal(err) 182 | } 183 | metrics := v1.Metrics{} 184 | err = pids.Stat("test", &metrics) 185 | if err == nil { 186 | t.Fatal("expected not nil err") 187 | } 188 | } 189 | 190 | func BenchmarkTestPids(b *testing.B) { 191 | 192 | mock, err := newMock(b) 193 | if err != nil { 194 | b.Fatal(err) 195 | } 196 | defer func() { 197 | if err := mock.delete(); err != nil { 198 | b.Errorf("failed delete: %v", err) 199 | } 200 | }() 201 | 202 | pids := NewPids(mock.root) 203 | if pids == nil { 204 | b.Fatal("pids is nil") 205 | } 206 | resources := specs.LinuxResources{ 207 | Pids: &specs.LinuxPids{ 208 | Limit: 10, 209 | }, 210 | } 211 | 212 | err = pids.Create("test", &resources) 213 | if err != nil { 214 | b.Fatal(err) 215 | } 216 | 217 | current := filepath.Join(mock.root, "pids", "test", "pids.current") 218 | if err = os.WriteFile( 219 | current, 220 | []byte(strconv.Itoa(5)), 221 | defaultFilePerm, 222 | ); err != nil { 223 | b.Fatal(err) 224 | } 225 | 226 | b.ReportAllocs() 227 | 228 | for i := 0; i < b.N; i++ { 229 | metrics := v1.Metrics{} 230 | err = pids.Stat("test", &metrics) 231 | if err != nil { 232 | b.Fatal(err) 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /cgroup1/rdma.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "math" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 27 | specs "github.com/opencontainers/runtime-spec/specs-go" 28 | ) 29 | 30 | type rdmaController struct { 31 | root string 32 | } 33 | 34 | func (p *rdmaController) Name() Name { 35 | return Rdma 36 | } 37 | 38 | func (p *rdmaController) Path(path string) string { 39 | return filepath.Join(p.root, path) 40 | } 41 | 42 | func NewRdma(root string) *rdmaController { 43 | return &rdmaController{ 44 | root: filepath.Join(root, string(Rdma)), 45 | } 46 | } 47 | 48 | func createCmdString(device string, limits *specs.LinuxRdma) string { 49 | var cmdString string 50 | 51 | cmdString = device 52 | if limits.HcaHandles != nil { 53 | cmdString = cmdString + " " + "hca_handle=" + strconv.FormatUint(uint64(*limits.HcaHandles), 10) 54 | } 55 | 56 | if limits.HcaObjects != nil { 57 | cmdString = cmdString + " " + "hca_object=" + strconv.FormatUint(uint64(*limits.HcaObjects), 10) 58 | } 59 | return cmdString 60 | } 61 | 62 | func (p *rdmaController) Create(path string, resources *specs.LinuxResources) error { 63 | if err := os.MkdirAll(p.Path(path), defaultDirPerm); err != nil { 64 | return err 65 | } 66 | 67 | for device, limit := range resources.Rdma { 68 | if device != "" && (limit.HcaHandles != nil || limit.HcaObjects != nil) { 69 | limit := limit 70 | return os.WriteFile( 71 | filepath.Join(p.Path(path), "rdma.max"), 72 | []byte(createCmdString(device, &limit)), 73 | defaultFilePerm, 74 | ) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func (p *rdmaController) Update(path string, resources *specs.LinuxResources) error { 81 | return p.Create(path, resources) 82 | } 83 | 84 | func parseRdmaKV(raw string, entry *v1.RdmaEntry) { 85 | var value uint64 86 | var err error 87 | 88 | parts := strings.Split(raw, "=") 89 | switch len(parts) { 90 | case 2: 91 | if parts[1] == "max" { 92 | value = math.MaxUint32 93 | } else { 94 | value, err = parseUint(parts[1], 10, 32) 95 | if err != nil { 96 | return 97 | } 98 | } 99 | if parts[0] == "hca_handle" { 100 | entry.HcaHandles = uint32(value) 101 | } else if parts[0] == "hca_object" { 102 | entry.HcaObjects = uint32(value) 103 | } 104 | } 105 | } 106 | 107 | func toRdmaEntry(strEntries []string) []*v1.RdmaEntry { 108 | var rdmaEntries []*v1.RdmaEntry 109 | for i := range strEntries { 110 | parts := strings.Fields(strEntries[i]) 111 | switch len(parts) { 112 | case 3: 113 | entry := new(v1.RdmaEntry) 114 | entry.Device = parts[0] 115 | parseRdmaKV(parts[1], entry) 116 | parseRdmaKV(parts[2], entry) 117 | 118 | rdmaEntries = append(rdmaEntries, entry) 119 | default: 120 | continue 121 | } 122 | } 123 | return rdmaEntries 124 | } 125 | 126 | func (p *rdmaController) Stat(path string, stats *v1.Metrics) error { 127 | currentData, err := os.ReadFile(filepath.Join(p.Path(path), "rdma.current")) 128 | if err != nil { 129 | return err 130 | } 131 | currentPerDevices := strings.Split(string(currentData), "\n") 132 | 133 | maxData, err := os.ReadFile(filepath.Join(p.Path(path), "rdma.max")) 134 | if err != nil { 135 | return err 136 | } 137 | maxPerDevices := strings.Split(string(maxData), "\n") 138 | 139 | // If device got removed between reading two files, ignore returning 140 | // stats. 141 | if len(currentPerDevices) != len(maxPerDevices) { 142 | return nil 143 | } 144 | 145 | currentEntries := toRdmaEntry(currentPerDevices) 146 | maxEntries := toRdmaEntry(maxPerDevices) 147 | 148 | stats.Rdma = &v1.RdmaStat{ 149 | Current: currentEntries, 150 | Limit: maxEntries, 151 | } 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /cgroup1/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | // State is a type that represents the state of the current cgroup 20 | type State string 21 | 22 | const ( 23 | Unknown State = "" 24 | Thawed State = "thawed" 25 | Frozen State = "frozen" 26 | Freezing State = "freezing" 27 | Deleted State = "deleted" 28 | ) 29 | -------------------------------------------------------------------------------- /cgroup1/stats/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package stats 18 | -------------------------------------------------------------------------------- /cgroup1/stats/metrics.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package io.containerd.cgroups.v1; 4 | 5 | option go_package = "github.com/containerd/cgroups/cgroup1/stats"; 6 | 7 | message Metrics { 8 | repeated HugetlbStat hugetlb = 1; 9 | PidsStat pids = 2; 10 | CPUStat cpu = 3; 11 | MemoryStat memory = 4; 12 | BlkIOStat blkio = 5; 13 | RdmaStat rdma = 6; 14 | repeated NetworkStat network = 7; 15 | CgroupStats cgroup_stats = 8; 16 | MemoryOomControl memory_oom_control = 9; 17 | } 18 | 19 | message HugetlbStat { 20 | uint64 usage = 1; 21 | uint64 max = 2; 22 | uint64 failcnt = 3; 23 | string pagesize = 4; 24 | } 25 | 26 | message PidsStat { 27 | uint64 current = 1; 28 | uint64 limit = 2; 29 | } 30 | 31 | message CPUStat { 32 | CPUUsage usage = 1; 33 | Throttle throttling = 2; 34 | } 35 | 36 | message CPUUsage { 37 | // values in nanoseconds 38 | uint64 total = 1; 39 | uint64 kernel = 2; 40 | uint64 user = 3; 41 | repeated uint64 per_cpu = 4; 42 | 43 | } 44 | 45 | message Throttle { 46 | uint64 periods = 1; 47 | uint64 throttled_periods = 2; 48 | uint64 throttled_time = 3; 49 | } 50 | 51 | message MemoryStat { 52 | uint64 cache = 1; 53 | uint64 rss = 2; 54 | uint64 rss_huge = 3; 55 | uint64 mapped_file = 4; 56 | uint64 dirty = 5; 57 | uint64 writeback = 6; 58 | uint64 pg_pg_in = 7; 59 | uint64 pg_pg_out = 8; 60 | uint64 pg_fault = 9; 61 | uint64 pg_maj_fault = 10; 62 | uint64 inactive_anon = 11; 63 | uint64 active_anon = 12; 64 | uint64 inactive_file = 13; 65 | uint64 active_file = 14; 66 | uint64 unevictable = 15; 67 | uint64 hierarchical_memory_limit = 16; 68 | uint64 hierarchical_swap_limit = 17; 69 | uint64 total_cache = 18; 70 | uint64 total_rss = 19; 71 | uint64 total_rss_huge = 20; 72 | uint64 total_mapped_file = 21; 73 | uint64 total_dirty = 22; 74 | uint64 total_writeback = 23; 75 | uint64 total_pg_pg_in = 24; 76 | uint64 total_pg_pg_out = 25; 77 | uint64 total_pg_fault = 26; 78 | uint64 total_pg_maj_fault = 27; 79 | uint64 total_inactive_anon = 28; 80 | uint64 total_active_anon = 29; 81 | uint64 total_inactive_file = 30; 82 | uint64 total_active_file = 31; 83 | uint64 total_unevictable = 32; 84 | MemoryEntry usage = 33; 85 | MemoryEntry swap = 34; 86 | MemoryEntry kernel = 35; 87 | MemoryEntry kernel_tcp = 36; 88 | 89 | } 90 | 91 | message MemoryEntry { 92 | uint64 limit = 1; 93 | uint64 usage = 2; 94 | uint64 max = 3; 95 | uint64 failcnt = 4; 96 | } 97 | 98 | message MemoryOomControl { 99 | uint64 oom_kill_disable = 1; 100 | uint64 under_oom = 2; 101 | uint64 oom_kill = 3; 102 | } 103 | 104 | message BlkIOStat { 105 | repeated BlkIOEntry io_service_bytes_recursive = 1; 106 | repeated BlkIOEntry io_serviced_recursive = 2; 107 | repeated BlkIOEntry io_queued_recursive = 3; 108 | repeated BlkIOEntry io_service_time_recursive = 4; 109 | repeated BlkIOEntry io_wait_time_recursive = 5; 110 | repeated BlkIOEntry io_merged_recursive = 6; 111 | repeated BlkIOEntry io_time_recursive = 7; 112 | repeated BlkIOEntry sectors_recursive = 8; 113 | } 114 | 115 | message BlkIOEntry { 116 | string op = 1; 117 | string device = 2; 118 | uint64 major = 3; 119 | uint64 minor = 4; 120 | uint64 value = 5; 121 | } 122 | 123 | message RdmaStat { 124 | repeated RdmaEntry current = 1; 125 | repeated RdmaEntry limit = 2; 126 | } 127 | 128 | message RdmaEntry { 129 | string device = 1; 130 | uint32 hca_handles = 2; 131 | uint32 hca_objects = 3; 132 | } 133 | 134 | message NetworkStat { 135 | string name = 1; 136 | uint64 rx_bytes = 2; 137 | uint64 rx_packets = 3; 138 | uint64 rx_errors = 4; 139 | uint64 rx_dropped = 5; 140 | uint64 tx_bytes = 6; 141 | uint64 tx_packets = 7; 142 | uint64 tx_errors = 8; 143 | uint64 tx_dropped = 9; 144 | } 145 | 146 | // CgroupStats exports per-cgroup statistics. 147 | message CgroupStats { 148 | // number of tasks sleeping 149 | uint64 nr_sleeping = 1; 150 | // number of tasks running 151 | uint64 nr_running = 2; 152 | // number of tasks in stopped state 153 | uint64 nr_stopped = 3; 154 | // number of tasks in uninterruptible state 155 | uint64 nr_uninterruptible = 4; 156 | // number of tasks waiting on IO 157 | uint64 nr_io_wait = 5; 158 | } 159 | -------------------------------------------------------------------------------- /cgroup1/subsystem.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | v1 "github.com/containerd/cgroups/v3/cgroup1/stats" 24 | "github.com/moby/sys/userns" 25 | specs "github.com/opencontainers/runtime-spec/specs-go" 26 | ) 27 | 28 | // Name is a typed name for a cgroup subsystem 29 | type Name string 30 | 31 | const ( 32 | Devices Name = "devices" 33 | Hugetlb Name = "hugetlb" 34 | Freezer Name = "freezer" 35 | Pids Name = "pids" 36 | NetCLS Name = "net_cls" 37 | NetPrio Name = "net_prio" 38 | PerfEvent Name = "perf_event" 39 | Cpuset Name = "cpuset" 40 | Cpu Name = "cpu" 41 | Cpuacct Name = "cpuacct" 42 | Memory Name = "memory" 43 | Blkio Name = "blkio" 44 | Rdma Name = "rdma" 45 | ) 46 | 47 | // Subsystems returns a complete list of the default cgroups 48 | // available on most linux systems 49 | func Subsystems() []Name { 50 | n := []Name{ 51 | Freezer, 52 | Pids, 53 | NetCLS, 54 | NetPrio, 55 | PerfEvent, 56 | Cpuset, 57 | Cpu, 58 | Cpuacct, 59 | Memory, 60 | Blkio, 61 | Rdma, 62 | } 63 | if !userns.RunningInUserNS() { 64 | n = append(n, Devices) 65 | } 66 | if _, err := os.Stat("/sys/kernel/mm/hugepages"); err == nil { 67 | n = append(n, Hugetlb) 68 | } 69 | return n 70 | } 71 | 72 | type Subsystem interface { 73 | Name() Name 74 | } 75 | 76 | type pather interface { 77 | Subsystem 78 | Path(path string) string 79 | } 80 | 81 | type creator interface { 82 | Subsystem 83 | Create(path string, resources *specs.LinuxResources) error 84 | } 85 | 86 | type deleter interface { 87 | Subsystem 88 | Delete(path string) error 89 | } 90 | 91 | type stater interface { 92 | Subsystem 93 | Stat(path string, stats *v1.Metrics) error 94 | } 95 | 96 | type updater interface { 97 | Subsystem 98 | Update(path string, resources *specs.LinuxResources) error 99 | } 100 | 101 | // SingleSubsystem returns a single cgroup subsystem within the base Hierarchy 102 | func SingleSubsystem(baseHierarchy Hierarchy, subsystem Name) Hierarchy { 103 | return func() ([]Subsystem, error) { 104 | subsystems, err := baseHierarchy() 105 | if err != nil { 106 | return nil, err 107 | } 108 | for _, s := range subsystems { 109 | if s.Name() == subsystem { 110 | return []Subsystem{ 111 | s, 112 | }, nil 113 | } 114 | } 115 | return nil, fmt.Errorf("unable to find subsystem %s", subsystem) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cgroup1/systemd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "strings" 23 | "sync" 24 | 25 | systemdDbus "github.com/coreos/go-systemd/v22/dbus" 26 | "github.com/godbus/dbus/v5" 27 | specs "github.com/opencontainers/runtime-spec/specs-go" 28 | ) 29 | 30 | const ( 31 | SystemdDbus Name = "systemd" 32 | defaultSlice Name = "system.slice" 33 | ) 34 | 35 | var ( 36 | canDelegate bool 37 | once sync.Once 38 | ) 39 | 40 | func Systemd() ([]Subsystem, error) { 41 | root, err := v1MountPoint() 42 | if err != nil { 43 | return nil, err 44 | } 45 | defaultSubsystems, err := defaults(root) 46 | if err != nil { 47 | return nil, err 48 | } 49 | s, err := NewSystemd(root) 50 | if err != nil { 51 | return nil, err 52 | } 53 | // make sure the systemd controller is added first 54 | return append([]Subsystem{s}, defaultSubsystems...), nil 55 | } 56 | 57 | func Slice(slice, name string) Path { 58 | if slice == "" { 59 | slice = string(defaultSlice) 60 | } 61 | return func(subsystem Name) (string, error) { 62 | return filepath.Join(slice, name), nil 63 | } 64 | } 65 | 66 | func NewSystemd(root string) (*SystemdController, error) { 67 | return &SystemdController{ 68 | root: root, 69 | }, nil 70 | } 71 | 72 | type SystemdController struct { 73 | root string 74 | } 75 | 76 | func (s *SystemdController) Name() Name { 77 | return SystemdDbus 78 | } 79 | 80 | func (s *SystemdController) Create(path string, _ *specs.LinuxResources) error { 81 | ctx := context.TODO() 82 | conn, err := systemdDbus.NewWithContext(ctx) 83 | if err != nil { 84 | return err 85 | } 86 | defer conn.Close() 87 | slice, name := splitName(path) 88 | // We need to see if systemd can handle the delegate property 89 | // Systemd will return an error if it cannot handle delegate regardless 90 | // of its bool setting. 91 | checkDelegate := func() { 92 | canDelegate = true 93 | dlSlice := newProperty("Delegate", true) 94 | if _, err := conn.StartTransientUnitContext(ctx, slice, "testdelegate", []systemdDbus.Property{dlSlice}, nil); err != nil { 95 | if dbusError, ok := err.(dbus.Error); ok { 96 | // Starting with systemd v237, Delegate is not even a property of slices anymore, 97 | // so the D-Bus call fails with "InvalidArgs" error. 98 | if strings.Contains(dbusError.Name, "org.freedesktop.DBus.Error.PropertyReadOnly") || strings.Contains(dbusError.Name, "org.freedesktop.DBus.Error.InvalidArgs") { 99 | canDelegate = false 100 | } 101 | } 102 | } 103 | 104 | _, _ = conn.StopUnitContext(ctx, slice, "testDelegate", nil) 105 | } 106 | once.Do(checkDelegate) 107 | properties := []systemdDbus.Property{ 108 | systemdDbus.PropDescription("cgroup " + name), 109 | systemdDbus.PropWants(slice), 110 | newProperty("DefaultDependencies", false), 111 | newProperty("MemoryAccounting", true), 112 | newProperty("CPUAccounting", true), 113 | newProperty("BlockIOAccounting", true), 114 | } 115 | 116 | // If we can delegate, we add the property back in 117 | if canDelegate { 118 | properties = append(properties, newProperty("Delegate", true)) 119 | } 120 | 121 | ch := make(chan string) 122 | _, err = conn.StartTransientUnitContext(ctx, name, "replace", properties, ch) 123 | if err != nil { 124 | return err 125 | } 126 | <-ch 127 | return nil 128 | } 129 | 130 | func (s *SystemdController) Delete(path string) error { 131 | ctx := context.TODO() 132 | conn, err := systemdDbus.NewWithContext(ctx) 133 | if err != nil { 134 | return err 135 | } 136 | defer conn.Close() 137 | _, name := splitName(path) 138 | ch := make(chan string) 139 | _, err = conn.StopUnitContext(ctx, name, "replace", ch) 140 | if err != nil { 141 | return err 142 | } 143 | <-ch 144 | return nil 145 | } 146 | 147 | func newProperty(name string, units interface{}) systemdDbus.Property { 148 | return systemdDbus.Property{ 149 | Name: name, 150 | Value: dbus.MakeVariant(units), 151 | } 152 | } 153 | 154 | func splitName(path string) (slice string, unit string) { 155 | slice, unit = filepath.Split(path) 156 | return strings.TrimSuffix(slice, "/"), unit 157 | } 158 | -------------------------------------------------------------------------------- /cgroup1/testutil_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "golang.org/x/sys/unix" 21 | ) 22 | 23 | var isUnified bool 24 | 25 | func init() { 26 | var st unix.Statfs_t 27 | if err := unix.Statfs("/sys/fs/cgroup", &st); err == nil { 28 | isUnified = st.Type == unix.CGROUP2_SUPER_MAGIC 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cgroup1/ticks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | func getClockTicks() uint64 { 20 | // The value comes from `C.sysconf(C._SC_CLK_TCK)`, and 21 | // on Linux it's a constant which is safe to be hard coded, 22 | // so we can avoid using cgo here. 23 | // See https://github.com/containerd/cgroups/pull/12 for 24 | // more details. 25 | return 100 26 | } 27 | -------------------------------------------------------------------------------- /cgroup1/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/containerd/cgroups/v3" 30 | units "github.com/docker/go-units" 31 | "github.com/moby/sys/userns" 32 | specs "github.com/opencontainers/runtime-spec/specs-go" 33 | ) 34 | 35 | // defaults returns all known groups 36 | func defaults(root string) ([]Subsystem, error) { 37 | h, err := NewHugetlb(root) 38 | if err != nil && !os.IsNotExist(err) { 39 | return nil, err 40 | } 41 | s := []Subsystem{ 42 | NewNamed(root, "systemd"), 43 | NewFreezer(root), 44 | NewPids(root), 45 | NewNetCls(root), 46 | NewNetPrio(root), 47 | NewPerfEvent(root), 48 | NewCpuset(root), 49 | NewCpu(root), 50 | NewCpuacct(root), 51 | NewMemory(root), 52 | NewBlkio(root), 53 | NewRdma(root), 54 | } 55 | // only add the devices cgroup if we are not in a user namespace 56 | // because modifications are not allowed 57 | if !userns.RunningInUserNS() { 58 | s = append(s, NewDevices(root)) 59 | } 60 | // add the hugetlb cgroup if error wasn't due to missing hugetlb 61 | // cgroup support on the host 62 | if err == nil { 63 | s = append(s, h) 64 | } 65 | return s, nil 66 | } 67 | 68 | // remove will remove a cgroup path handling EAGAIN and EBUSY errors and 69 | // retrying the remove after a exp timeout 70 | func remove(path string) error { 71 | delay := 10 * time.Millisecond 72 | for i := 0; i < 5; i++ { 73 | if i != 0 { 74 | time.Sleep(delay) 75 | delay *= 2 76 | } 77 | if err := os.RemoveAll(path); err == nil { 78 | return nil 79 | } 80 | } 81 | return fmt.Errorf("cgroups: unable to remove path %q", path) 82 | } 83 | 84 | // readPids will read all the pids of processes or tasks in a cgroup by the provided path 85 | func readPids(path string, subsystem Name, pType procType) ([]Process, error) { 86 | f, err := os.Open(filepath.Join(path, pType)) 87 | if err != nil { 88 | return nil, err 89 | } 90 | defer f.Close() 91 | var ( 92 | out []Process 93 | s = bufio.NewScanner(f) 94 | ) 95 | for s.Scan() { 96 | if t := s.Text(); t != "" { 97 | pid, err := strconv.Atoi(t) 98 | if err != nil { 99 | return nil, err 100 | } 101 | out = append(out, Process{ 102 | Pid: pid, 103 | Subsystem: subsystem, 104 | Path: path, 105 | }) 106 | } 107 | } 108 | if err := s.Err(); err != nil { 109 | // failed to read all pids? 110 | return nil, err 111 | } 112 | return out, nil 113 | } 114 | 115 | func hugePageSizes() ([]string, error) { 116 | var ( 117 | pageSizes []string 118 | sizeList = []string{"B", "KB", "MB", "GB", "TB", "PB"} 119 | ) 120 | files, err := os.ReadDir("/sys/kernel/mm/hugepages") 121 | if err != nil { 122 | return nil, err 123 | } 124 | for _, st := range files { 125 | nameArray := strings.Split(st.Name(), "-") 126 | pageSize, err := units.RAMInBytes(nameArray[1]) 127 | if err != nil { 128 | return nil, err 129 | } 130 | pageSizes = append(pageSizes, units.CustomSize("%g%s", float64(pageSize), 1024.0, sizeList)) 131 | } 132 | return pageSizes, nil 133 | } 134 | 135 | func readUint(path string) (uint64, error) { 136 | f, err := os.Open(path) 137 | if err != nil { 138 | return 0, err 139 | } 140 | defer f.Close() 141 | 142 | // We should only need 20 bytes for the max uint64, but for a nice power of 2 143 | // lets use 32. 144 | b := make([]byte, 32) 145 | n, err := f.Read(b) 146 | if err != nil { 147 | return 0, err 148 | } 149 | s := string(bytes.TrimSpace(b[:n])) 150 | if s == "max" { 151 | // Return 0 for the max value to maintain backward compatibility. 152 | return 0, nil 153 | } 154 | return parseUint(s, 10, 64) 155 | } 156 | 157 | func parseUint(s string, base, bitSize int) (uint64, error) { 158 | v, err := strconv.ParseUint(s, base, bitSize) 159 | if err != nil { 160 | intValue, intErr := strconv.ParseInt(s, base, bitSize) 161 | // 1. Handle negative values greater than MinInt64 (and) 162 | // 2. Handle negative values lesser than MinInt64 163 | if intErr == nil && intValue < 0 { 164 | return 0, nil 165 | } else if intErr != nil && 166 | intErr.(*strconv.NumError).Err == strconv.ErrRange && 167 | intValue < 0 { 168 | return 0, nil 169 | } 170 | return 0, err 171 | } 172 | return v, nil 173 | } 174 | 175 | func parseKV(raw string) (string, uint64, error) { 176 | parts := strings.Fields(raw) 177 | switch len(parts) { 178 | case 2: 179 | v, err := parseUint(parts[1], 10, 64) 180 | if err != nil { 181 | return "", 0, err 182 | } 183 | return parts[0], v, nil 184 | default: 185 | return "", 0, ErrInvalidFormat 186 | } 187 | } 188 | 189 | // ParseCgroupFile parses the given cgroup file, typically /proc/self/cgroup 190 | // or /proc//cgroup, into a map of subsystems to cgroup paths, e.g. 191 | // 192 | // "cpu": "/user.slice/user-1000.slice" 193 | // "pids": "/user.slice/user-1000.slice" 194 | // 195 | // etc. 196 | // 197 | // The resulting map does not have an element for cgroup v2 unified hierarchy. 198 | // Use [cgroups.ParseCgroupFileUnified] to get the unified path. 199 | func ParseCgroupFile(path string) (map[string]string, error) { 200 | x, _, err := cgroups.ParseCgroupFileUnified(path) 201 | return x, err 202 | } 203 | 204 | // ParseCgroupFileUnified returns legacy subsystem paths as the first value, 205 | // and returns the unified path as the second value. 206 | // 207 | // Deprecated: use [cgroups.ParseCgroupFileUnified] instead . 208 | func ParseCgroupFileUnified(path string) (map[string]string, string, error) { 209 | return cgroups.ParseCgroupFileUnified(path) 210 | } 211 | 212 | func getCgroupDestination(subsystem string) (string, error) { 213 | f, err := os.Open("/proc/self/mountinfo") 214 | if err != nil { 215 | return "", err 216 | } 217 | defer f.Close() 218 | s := bufio.NewScanner(f) 219 | for s.Scan() { 220 | fields := strings.Split(s.Text(), " ") 221 | if len(fields) < 10 { 222 | // broken mountinfo? 223 | continue 224 | } 225 | if fields[len(fields)-3] != "cgroup" { 226 | continue 227 | } 228 | for _, opt := range strings.Split(fields[len(fields)-1], ",") { 229 | if opt == subsystem { 230 | return fields[3], nil 231 | } 232 | } 233 | } 234 | if err := s.Err(); err != nil { 235 | return "", err 236 | } 237 | return "", ErrNoCgroupMountDestination 238 | } 239 | 240 | func pathers(subsystems []Subsystem) []pather { 241 | var out []pather 242 | for _, s := range subsystems { 243 | if p, ok := s.(pather); ok { 244 | out = append(out, p) 245 | } 246 | } 247 | return out 248 | } 249 | 250 | func initializeSubsystem(s Subsystem, path Path, resources *specs.LinuxResources) error { 251 | if c, ok := s.(creator); ok { 252 | p, err := path(s.Name()) 253 | if err != nil { 254 | return err 255 | } 256 | if err := c.Create(p, resources); err != nil { 257 | return err 258 | } 259 | } else if c, ok := s.(pather); ok { 260 | p, err := path(s.Name()) 261 | if err != nil { 262 | return err 263 | } 264 | // do the default create if the group does not have a custom one 265 | if err := os.MkdirAll(c.Path(p), defaultDirPerm); err != nil { 266 | return err 267 | } 268 | } 269 | return nil 270 | } 271 | 272 | func cleanPath(path string) string { 273 | if path == "" { 274 | return "" 275 | } 276 | path = filepath.Clean(path) 277 | if !filepath.IsAbs(path) { 278 | path, _ = filepath.Rel(string(os.PathSeparator), filepath.Clean(string(os.PathSeparator)+path)) 279 | } 280 | return path 281 | } 282 | -------------------------------------------------------------------------------- /cgroup1/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | ) 24 | 25 | func BenchmarkReaduint64(b *testing.B) { 26 | b.ReportAllocs() 27 | 28 | for i := 0; i < b.N; i++ { 29 | _, err := readUint("/proc/self/loginuid") 30 | if err != nil { 31 | b.Fatal(err) 32 | } 33 | } 34 | } 35 | 36 | func TestReadUint(t *testing.T) { 37 | tDir := t.TempDir() 38 | pidsmax := filepath.Join(tDir, "pids.max") 39 | err := os.WriteFile(pidsmax, []byte("max"), 0644) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | max, err := readUint(pidsmax) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | // test for backwards compatibility 48 | if max != 0 { 49 | t.Fail() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cgroup1/v1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup1 18 | 19 | import ( 20 | "bufio" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | ) 26 | 27 | // Default returns all the groups in the default cgroups mountpoint in a single hierarchy 28 | func Default() ([]Subsystem, error) { 29 | root, err := v1MountPoint() 30 | if err != nil { 31 | return nil, err 32 | } 33 | subsystems, err := defaults(root) 34 | if err != nil { 35 | return nil, err 36 | } 37 | var enabled []Subsystem 38 | for _, s := range pathers(subsystems) { 39 | // check and remove the default groups that do not exist 40 | if _, err := os.Lstat(s.Path("/")); err == nil { 41 | enabled = append(enabled, s) 42 | } 43 | } 44 | return enabled, nil 45 | } 46 | 47 | // v1MountPoint returns the mount point where the cgroup 48 | // mountpoints are mounted in a single hierarchy 49 | func v1MountPoint() (string, error) { 50 | f, err := os.Open("/proc/self/mountinfo") 51 | if err != nil { 52 | return "", err 53 | } 54 | defer f.Close() 55 | scanner := bufio.NewScanner(f) 56 | for scanner.Scan() { 57 | var ( 58 | text = scanner.Text() 59 | fields = strings.Split(text, " ") 60 | numFields = len(fields) 61 | ) 62 | if numFields < 10 { 63 | return "", fmt.Errorf("mountinfo: bad entry %q", text) 64 | } 65 | if fields[numFields-3] == "cgroup" { 66 | return filepath.Dir(fields[4]), nil 67 | } 68 | } 69 | if err := scanner.Err(); err != nil { 70 | return "", err 71 | } 72 | return "", ErrMountPointNotExist 73 | } 74 | -------------------------------------------------------------------------------- /cgroup2/cpu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "math" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | type CPUMax string 26 | 27 | func NewCPUMax(quota *int64, period *uint64) CPUMax { 28 | max := "max" 29 | if quota != nil { 30 | max = strconv.FormatInt(*quota, 10) 31 | } 32 | return CPUMax(strings.Join([]string{max, strconv.FormatUint(*period, 10)}, " ")) 33 | } 34 | 35 | type CPU struct { 36 | Weight *uint64 37 | Max CPUMax 38 | Cpus string 39 | Mems string 40 | } 41 | 42 | func (c CPUMax) extractQuotaAndPeriod() (int64, uint64) { 43 | var ( 44 | quota int64 45 | period uint64 46 | ) 47 | values := strings.Split(string(c), " ") 48 | if values[0] == "max" { 49 | quota = math.MaxInt64 50 | } else { 51 | quota, _ = strconv.ParseInt(values[0], 10, 64) 52 | } 53 | period, _ = strconv.ParseUint(values[1], 10, 64) 54 | return quota, period 55 | } 56 | 57 | func (r *CPU) Values() (o []Value) { 58 | if r.Weight != nil { 59 | o = append(o, Value{ 60 | filename: "cpu.weight", 61 | value: *r.Weight, 62 | }) 63 | } 64 | if r.Max != "" { 65 | o = append(o, Value{ 66 | filename: "cpu.max", 67 | value: r.Max, 68 | }) 69 | } 70 | if r.Cpus != "" { 71 | o = append(o, Value{ 72 | filename: "cpuset.cpus", 73 | value: r.Cpus, 74 | }) 75 | } 76 | if r.Mems != "" { 77 | o = append(o, Value{ 78 | filename: "cpuset.mems", 79 | value: r.Mems, 80 | }) 81 | } 82 | return o 83 | } 84 | -------------------------------------------------------------------------------- /cgroup2/cpuv2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "math" 22 | "os" 23 | "strconv" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestCgroupv2CpuStats(t *testing.T) { 31 | checkCgroupMode(t) 32 | group := "/cpu-test-cg" 33 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 34 | var ( 35 | quota int64 = 10000 36 | period uint64 = 8000 37 | weight uint64 = 100 38 | ) 39 | 40 | c, err := NewManager(defaultCgroup2Path, groupPath, &Resources{ 41 | CPU: &CPU{ 42 | Weight: &weight, 43 | Max: NewCPUMax("a, &period), 44 | Cpus: "0", 45 | Mems: "0", 46 | }, 47 | }) 48 | require.NoError(t, err, "failed to init new cgroup manager") 49 | t.Cleanup(func() { 50 | _ = os.Remove(c.path) 51 | }) 52 | 53 | checkFileContent(t, c.path, "cpu.weight", strconv.FormatUint(weight, 10)) 54 | checkFileContent(t, c.path, "cpu.max", "10000 8000") 55 | checkFileContent(t, c.path, "cpuset.cpus", "0") 56 | checkFileContent(t, c.path, "cpuset.mems", "0") 57 | } 58 | 59 | func TestSystemdCgroupCpuController(t *testing.T) { 60 | checkCgroupMode(t) 61 | group := fmt.Sprintf("testing-cpu-%d.scope", os.Getpid()) 62 | var weight uint64 = 100 63 | c, err := NewSystemd("", group, os.Getpid(), &Resources{CPU: &CPU{Weight: &weight}}) 64 | require.NoError(t, err, "failed to init new cgroup systemd manager") 65 | 66 | checkFileContent(t, c.path, "cpu.weight", strconv.FormatUint(weight, 10)) 67 | } 68 | 69 | func TestSystemdCgroupCpuController_NilWeight(t *testing.T) { 70 | checkCgroupMode(t) 71 | group := "testingCpuNilWeight.slice" 72 | // nil weight defaults to 100 73 | var quota int64 = 10000 74 | var period uint64 = 8000 75 | cpuMax := NewCPUMax("a, &period) 76 | _, err := NewSystemd("/", group, -1, &Resources{ 77 | CPU: &CPU{ 78 | Weight: nil, 79 | Max: cpuMax, 80 | }, 81 | }) 82 | require.NoError(t, err, "failed to init new cgroup systemd manager") 83 | } 84 | 85 | func TestExtractQuotaAndPeriod(t *testing.T) { 86 | var ( 87 | period uint64 88 | quota int64 89 | ) 90 | quota = 10000 91 | period = 8000 92 | cpuMax := NewCPUMax("a, &period) 93 | tquota, tPeriod := cpuMax.extractQuotaAndPeriod() 94 | 95 | assert.Equal(t, quota, tquota) 96 | assert.Equal(t, period, tPeriod) 97 | 98 | // case with nil quota which makes it "max" - max int val 99 | cpuMax2 := NewCPUMax(nil, &period) 100 | tquota2, tPeriod2 := cpuMax2.extractQuotaAndPeriod() 101 | 102 | assert.Equal(t, int64(math.MaxInt64), tquota2) 103 | assert.Equal(t, period, tPeriod2) 104 | } 105 | -------------------------------------------------------------------------------- /cgroup2/devicefilter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Devicefilter contains eBPF device filter program 18 | // 19 | // The implementation is based on https://github.com/containers/crun/blob/0.10.2/src/libcrun/ebpf.c 20 | // 21 | // Although ebpf.c is originally licensed under LGPL-3.0-or-later, the author (Giuseppe Scrivano) 22 | // agreed to relicense the file in Apache License 2.0: https://github.com/opencontainers/runc/issues/2144#issuecomment-543116397 23 | // 24 | // This particular Go implementation based on runc version 25 | // https://github.com/opencontainers/runc/blob/master/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go 26 | 27 | package cgroup2 28 | 29 | import ( 30 | "errors" 31 | "fmt" 32 | "math" 33 | 34 | "github.com/cilium/ebpf/asm" 35 | "github.com/opencontainers/runtime-spec/specs-go" 36 | "golang.org/x/sys/unix" 37 | ) 38 | 39 | const ( 40 | // license string format is same as kernel MODULE_LICENSE macro 41 | license = "Apache" 42 | ) 43 | 44 | // DeviceFilter returns eBPF device filter program and its license string 45 | func DeviceFilter(devices []specs.LinuxDeviceCgroup) (asm.Instructions, string, error) { 46 | p := &program{} 47 | p.init() 48 | for i := len(devices) - 1; i >= 0; i-- { 49 | if err := p.appendDevice(devices[i]); err != nil { 50 | return nil, "", err 51 | } 52 | } 53 | insts, err := p.finalize() 54 | return insts, license, err 55 | } 56 | 57 | type program struct { 58 | insts asm.Instructions 59 | hasWildCard bool 60 | blockID int 61 | } 62 | 63 | func (p *program) init() { 64 | // struct bpf_cgroup_dev_ctx: https://elixir.bootlin.com/linux/v5.3.6/source/include/uapi/linux/bpf.h#L3423 65 | /* 66 | u32 access_type 67 | u32 major 68 | u32 minor 69 | */ 70 | // R2 <- type (lower 16 bit of u32 access_type at R1[0]) 71 | p.insts = append(p.insts, 72 | asm.LoadMem(asm.R2, asm.R1, 0, asm.Half)) 73 | 74 | // R3 <- access (upper 16 bit of u32 access_type at R1[0]) 75 | p.insts = append(p.insts, 76 | asm.LoadMem(asm.R3, asm.R1, 0, asm.Word), 77 | // RSh: bitwise shift right 78 | asm.RSh.Imm32(asm.R3, 16)) 79 | 80 | // R4 <- major (u32 major at R1[4]) 81 | p.insts = append(p.insts, 82 | asm.LoadMem(asm.R4, asm.R1, 4, asm.Word)) 83 | 84 | // R5 <- minor (u32 minor at R1[8]) 85 | p.insts = append(p.insts, 86 | asm.LoadMem(asm.R5, asm.R1, 8, asm.Word)) 87 | } 88 | 89 | // appendDevice needs to be called from the last element of OCI linux.resources.devices to the head element. 90 | func (p *program) appendDevice(dev specs.LinuxDeviceCgroup) error { 91 | if p.blockID < 0 { 92 | return errors.New("the program is finalized") 93 | } 94 | if p.hasWildCard { 95 | // All entries after wildcard entry are ignored 96 | return nil 97 | } 98 | 99 | bpfType := int32(-1) 100 | hasType := true 101 | switch dev.Type { 102 | case string('c'): 103 | bpfType = int32(unix.BPF_DEVCG_DEV_CHAR) 104 | case string('b'): 105 | bpfType = int32(unix.BPF_DEVCG_DEV_BLOCK) 106 | case string('a'): 107 | hasType = false 108 | default: 109 | // if not specified in OCI json, typ is set to DeviceTypeAll 110 | return fmt.Errorf("invalid DeviceType %q", dev.Type) 111 | } 112 | if *dev.Major > math.MaxUint32 { 113 | return fmt.Errorf("invalid major %d", *dev.Major) 114 | } 115 | if *dev.Minor > math.MaxUint32 { 116 | return fmt.Errorf("invalid minor %d", *dev.Major) 117 | } 118 | hasMajor := *dev.Major >= 0 // if not specified in OCI json, major is set to -1 119 | hasMinor := *dev.Minor >= 0 120 | bpfAccess := int32(0) 121 | for _, r := range dev.Access { 122 | switch r { 123 | case 'r': 124 | bpfAccess |= unix.BPF_DEVCG_ACC_READ 125 | case 'w': 126 | bpfAccess |= unix.BPF_DEVCG_ACC_WRITE 127 | case 'm': 128 | bpfAccess |= unix.BPF_DEVCG_ACC_MKNOD 129 | default: 130 | return fmt.Errorf("unknown device access %v", r) 131 | } 132 | } 133 | // If the access is rwm, skip the check. 134 | hasAccess := bpfAccess != (unix.BPF_DEVCG_ACC_READ | unix.BPF_DEVCG_ACC_WRITE | unix.BPF_DEVCG_ACC_MKNOD) 135 | 136 | blockSym := fmt.Sprintf("block-%d", p.blockID) 137 | nextBlockSym := fmt.Sprintf("block-%d", p.blockID+1) 138 | prevBlockLastIdx := len(p.insts) - 1 139 | if hasType { 140 | p.insts = append(p.insts, 141 | // if (R2 != bpfType) goto next 142 | asm.JNE.Imm(asm.R2, bpfType, nextBlockSym), 143 | ) 144 | } 145 | if hasAccess { 146 | p.insts = append(p.insts, 147 | // if (R3 & bpfAccess == 0 /* use R1 as a temp var */) goto next 148 | asm.Mov.Reg32(asm.R1, asm.R3), 149 | asm.And.Imm32(asm.R1, bpfAccess), 150 | asm.JEq.Imm(asm.R1, 0, nextBlockSym), 151 | ) 152 | } 153 | if hasMajor { 154 | p.insts = append(p.insts, 155 | // if (R4 != major) goto next 156 | asm.JNE.Imm(asm.R4, int32(*dev.Major), nextBlockSym), 157 | ) 158 | } 159 | if hasMinor { 160 | p.insts = append(p.insts, 161 | // if (R5 != minor) goto next 162 | asm.JNE.Imm(asm.R5, int32(*dev.Minor), nextBlockSym), 163 | ) 164 | } 165 | if !hasType && !hasAccess && !hasMajor && !hasMinor { 166 | p.hasWildCard = true 167 | } 168 | p.insts = append(p.insts, acceptBlock(dev.Allow)...) 169 | // set blockSym to the first instruction we added in this iteration 170 | p.insts[prevBlockLastIdx+1] = p.insts[prevBlockLastIdx+1].WithSymbol(blockSym) 171 | p.blockID++ 172 | return nil 173 | } 174 | 175 | func (p *program) finalize() (asm.Instructions, error) { 176 | if p.hasWildCard { 177 | // acceptBlock with asm.Return() is already inserted 178 | return p.insts, nil 179 | } 180 | blockSym := fmt.Sprintf("block-%d", p.blockID) 181 | p.insts = append(p.insts, 182 | // R0 <- 0 183 | asm.Mov.Imm32(asm.R0, 0).WithSymbol(blockSym), 184 | asm.Return(), 185 | ) 186 | p.blockID = -1 187 | return p.insts, nil 188 | } 189 | 190 | func acceptBlock(accept bool) asm.Instructions { 191 | v := int32(0) 192 | if accept { 193 | v = 1 194 | } 195 | return []asm.Instruction{ 196 | // R0 <- v 197 | asm.Mov.Imm32(asm.R0, v), 198 | asm.Return(), 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /cgroup2/devicefilter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | 23 | "github.com/opencontainers/runtime-spec/specs-go" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func hash(s, comm string) string { 28 | var res []string 29 | for _, l := range strings.Split(s, "\n") { 30 | trimmed := strings.TrimSpace(l) 31 | if trimmed == "" || strings.HasPrefix(trimmed, comm) { 32 | continue 33 | } 34 | res = append(res, trimmed) 35 | } 36 | return strings.Join(res, "\n") 37 | } 38 | 39 | func testDeviceFilter(t testing.TB, devices []specs.LinuxDeviceCgroup, expectedStr string) { 40 | insts, _, err := DeviceFilter(devices) 41 | require.NoErrorf(t, err, "%s: (devices: %+v)", t.Name(), devices) 42 | 43 | s := insts.String() 44 | t.Logf("%s: devices: %+v\n%s", t.Name(), devices, s) 45 | if expectedStr != "" { 46 | hashed := hash(s, "//") 47 | expectedHashed := hash(expectedStr, "//") 48 | require.Equal(t, expectedHashed, hashed) 49 | } 50 | } 51 | 52 | func TestDeviceFilter_Nil(t *testing.T) { 53 | expected := ` 54 | // load parameters into registers 55 | 0: LdXMemH dst: r2 src: r1 off: 0 imm: 0 56 | 1: LdXMemW dst: r3 src: r1 off: 0 imm: 0 57 | 2: RShImm32 dst: r3 imm: 16 58 | 3: LdXMemW dst: r4 src: r1 off: 4 imm: 0 59 | 4: LdXMemW dst: r5 src: r1 off: 8 imm: 0 60 | block-0: 61 | // return 0 (reject) 62 | 5: MovImm32 dst: r0 imm: 0 63 | 6: Exit 64 | ` 65 | testDeviceFilter(t, nil, expected) 66 | } 67 | 68 | func TestDeviceFilter_Privileged(t *testing.T) { 69 | devices := []specs.LinuxDeviceCgroup{ 70 | { 71 | Type: "a", 72 | Major: pointerInt64(-1), 73 | Minor: pointerInt64(-1), 74 | Access: "rwm", 75 | Allow: true, 76 | }, 77 | } 78 | expected := ` 79 | // load parameters into registers 80 | 0: LdXMemH dst: r2 src: r1 off: 0 imm: 0 81 | 1: LdXMemW dst: r3 src: r1 off: 0 imm: 0 82 | 2: RShImm32 dst: r3 imm: 16 83 | 3: LdXMemW dst: r4 src: r1 off: 4 imm: 0 84 | 4: LdXMemW dst: r5 src: r1 off: 8 imm: 0 85 | block-0: 86 | // return 1 (accept) 87 | 5: MovImm32 dst: r0 imm: 1 88 | 6: Exit 89 | ` 90 | testDeviceFilter(t, devices, expected) 91 | } 92 | 93 | func TestDeviceFilter_PrivilegedExceptSingleDevice(t *testing.T) { 94 | devices := []specs.LinuxDeviceCgroup{ 95 | { 96 | Type: "a", 97 | Major: pointerInt64(-1), 98 | Minor: pointerInt64(-1), 99 | Access: "rwm", 100 | Allow: true, 101 | }, 102 | { 103 | Type: "b", 104 | Major: pointerInt64(8), 105 | Minor: pointerInt64(0), 106 | Access: "rwm", 107 | Allow: false, 108 | }, 109 | } 110 | expected := ` 111 | // load parameters into registers 112 | 0: LdXMemH dst: r2 src: r1 off: 0 imm: 0 113 | 1: LdXMemW dst: r3 src: r1 off: 0 imm: 0 114 | 2: RShImm32 dst: r3 imm: 16 115 | 3: LdXMemW dst: r4 src: r1 off: 4 imm: 0 116 | 4: LdXMemW dst: r5 src: r1 off: 8 imm: 0 117 | block-0: 118 | // return 0 (reject) if type==b && major == 8 && minor == 0 119 | 5: JNEImm dst: r2 off: -1 imm: 1 120 | 6: JNEImm dst: r4 off: -1 imm: 8 121 | 7: JNEImm dst: r5 off: -1 imm: 0 122 | 8: MovImm32 dst: r0 imm: 0 123 | 9: Exit 124 | block-1: 125 | // return 1 (accept) 126 | 10: MovImm32 dst: r0 imm: 1 127 | 11: Exit 128 | ` 129 | testDeviceFilter(t, devices, expected) 130 | } 131 | 132 | func TestDeviceFilter_Weird(t *testing.T) { 133 | devices := []specs.LinuxDeviceCgroup{ 134 | { 135 | Type: "b", 136 | Major: pointerInt64(8), 137 | Minor: pointerInt64(1), 138 | Access: "rwm", 139 | Allow: false, 140 | }, 141 | { 142 | Type: "a", 143 | Major: pointerInt64(-1), 144 | Minor: pointerInt64(-1), 145 | Access: "rwm", 146 | Allow: true, 147 | }, 148 | { 149 | Type: "b", 150 | Major: pointerInt64(8), 151 | Minor: pointerInt64(2), 152 | Access: "rwm", 153 | Allow: false, 154 | }, 155 | } 156 | // 8/1 is allowed, 8/2 is not allowed. 157 | // This conforms to runc v1.0.0-rc.9 (cgroup1) behavior. 158 | expected := ` 159 | // load parameters into registers 160 | 0: LdXMemH dst: r2 src: r1 off: 0 imm: 0 161 | 1: LdXMemW dst: r3 src: r1 off: 0 imm: 0 162 | 2: RShImm32 dst: r3 imm: 16 163 | 3: LdXMemW dst: r4 src: r1 off: 4 imm: 0 164 | 4: LdXMemW dst: r5 src: r1 off: 8 imm: 0 165 | block-0: 166 | // return 0 (reject) if type==b && major == 8 && minor == 2 167 | 5: JNEImm dst: r2 off: -1 imm: 1 168 | 6: JNEImm dst: r4 off: -1 imm: 8 169 | 7: JNEImm dst: r5 off: -1 imm: 2 170 | 8: MovImm32 dst: r0 imm: 0 171 | 9: Exit 172 | block-1: 173 | // return 1 (accept) 174 | 10: MovImm32 dst: r0 imm: 1 175 | 11: Exit 176 | ` 177 | testDeviceFilter(t, devices, expected) 178 | } 179 | 180 | func pointerInt64(int int64) *int64 { 181 | return &int 182 | } 183 | -------------------------------------------------------------------------------- /cgroup2/ebpf.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/cilium/ebpf" 23 | "github.com/cilium/ebpf/asm" 24 | "github.com/cilium/ebpf/link" 25 | "github.com/opencontainers/runtime-spec/specs-go" 26 | "golang.org/x/sys/unix" 27 | ) 28 | 29 | // LoadAttachCgroupDeviceFilter installs eBPF device filter program to /sys/fs/cgroup/ directory. 30 | // 31 | // Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 . 32 | // 33 | // https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92 34 | func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFD int) (func() error, error) { 35 | nilCloser := func() error { 36 | return nil 37 | } 38 | spec := &ebpf.ProgramSpec{ 39 | Type: ebpf.CGroupDevice, 40 | Instructions: insts, 41 | License: license, 42 | } 43 | prog, err := ebpf.NewProgram(spec) 44 | if err != nil { 45 | return nilCloser, err 46 | } 47 | err = link.RawAttachProgram(link.RawAttachProgramOptions{ 48 | Target: dirFD, 49 | Program: prog, 50 | Attach: ebpf.AttachCGroupDevice, 51 | Flags: unix.BPF_F_ALLOW_MULTI, 52 | }) 53 | if err != nil { 54 | return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err) 55 | } 56 | closer := func() error { 57 | err = link.RawDetachProgram(link.RawDetachProgramOptions{ 58 | Target: dirFD, 59 | Program: prog, 60 | Attach: ebpf.AttachCGroupDevice, 61 | }) 62 | if err != nil { 63 | return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err) 64 | } 65 | return nil 66 | } 67 | return closer, nil 68 | } 69 | 70 | func isRWM(cgroupPermissions string) bool { 71 | r := false 72 | w := false 73 | m := false 74 | for _, rn := range cgroupPermissions { 75 | switch rn { 76 | case 'r': 77 | r = true 78 | case 'w': 79 | w = true 80 | case 'm': 81 | m = true 82 | } 83 | } 84 | return r && w && m 85 | } 86 | 87 | // the logic is from runc 88 | // https://github.com/opencontainers/runc/blob/master/libcontainer/cgroups/fs/devices_v2.go#L44 89 | func canSkipEBPFError(devices []specs.LinuxDeviceCgroup) bool { 90 | for _, dev := range devices { 91 | if dev.Allow || !isRWM(dev.Access) { 92 | return false 93 | } 94 | } 95 | return true 96 | } 97 | -------------------------------------------------------------------------------- /cgroup2/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "errors" 21 | ) 22 | 23 | var ( 24 | ErrInvalidFormat = errors.New("cgroups: parsing file with invalid format failed") 25 | ErrInvalidGroupPath = errors.New("cgroups: invalid group path") 26 | ) 27 | -------------------------------------------------------------------------------- /cgroup2/hugetlb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import "strings" 20 | 21 | type HugeTlb []HugeTlbEntry 22 | 23 | type HugeTlbEntry struct { 24 | HugePageSize string 25 | Limit uint64 26 | } 27 | 28 | func (r *HugeTlb) Values() (o []Value) { 29 | for _, e := range *r { 30 | o = append(o, Value{ 31 | filename: strings.Join([]string{"hugetlb", e.HugePageSize, "max"}, "."), 32 | value: e.Limit, 33 | }) 34 | } 35 | 36 | return o 37 | } 38 | -------------------------------------------------------------------------------- /cgroup2/hugetlbv2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestCgroupv2HugetlbStats(t *testing.T) { 29 | checkCgroupControllerSupported(t, "hugetlb") 30 | checkCgroupMode(t) 31 | group := "/hugetlb-test-cg" 32 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 33 | hugeTlb := HugeTlb{HugeTlbEntry{HugePageSize: "2MB", Limit: 1073741824}} 34 | res := Resources{ 35 | HugeTlb: &hugeTlb, 36 | } 37 | c, err := NewManager(defaultCgroup2Path, groupPath, &res) 38 | require.NoError(t, err, "failed to init new cgroup manager") 39 | t.Cleanup(func() { 40 | os.Remove(c.path) 41 | }) 42 | 43 | stats, err := c.Stat() 44 | require.NoError(t, err, "failed to get cgroup stats") 45 | 46 | for _, entry := range stats.Hugetlb { 47 | if entry.Pagesize == "2MB" { 48 | assert.Equal(t, uint64(1073741824), entry.Max) 49 | break 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cgroup2/io.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import "fmt" 20 | 21 | type IOType string 22 | 23 | const ( 24 | ReadBPS IOType = "rbps" 25 | WriteBPS IOType = "wbps" 26 | ReadIOPS IOType = "riops" 27 | WriteIOPS IOType = "wiops" 28 | ) 29 | 30 | type BFQ struct { 31 | Weight uint16 32 | } 33 | 34 | type Entry struct { 35 | Type IOType 36 | Major int64 37 | Minor int64 38 | Rate uint64 39 | } 40 | 41 | func (e Entry) String() string { 42 | return fmt.Sprintf("%d:%d %s=%d", e.Major, e.Minor, e.Type, e.Rate) 43 | } 44 | 45 | type IO struct { 46 | BFQ BFQ 47 | Max []Entry 48 | } 49 | 50 | func (i *IO) Values() (o []Value) { 51 | if i.BFQ.Weight != 0 { 52 | o = append(o, Value{ 53 | filename: "io.bfq.weight", 54 | value: i.BFQ.Weight, 55 | }) 56 | } 57 | for _, e := range i.Max { 58 | o = append(o, Value{ 59 | filename: "io.max", 60 | value: e.String(), 61 | }) 62 | } 63 | return o 64 | } 65 | -------------------------------------------------------------------------------- /cgroup2/iov2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestCgroupv2IOController(t *testing.T) { 28 | t.Skip("FIXME: this test doesn't work on Fedora 32 Vagrant: TestCgroupv2IOController: iov2_test.go:42: failed to init new cgroup manager: write /sys/fs/cgroup/io-test-cg-22708/io.max: no such device") 29 | checkCgroupMode(t) 30 | group := "/io-test-cg" 31 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 32 | var ( 33 | // weight uint16 = 100 34 | maj int64 = 8 35 | min int64 = 0 36 | rate uint64 = 120 37 | ) 38 | res := Resources{ 39 | IO: &IO{ 40 | Max: []Entry{{Major: maj, Minor: min, Type: ReadIOPS, Rate: rate}}, 41 | }, 42 | } 43 | c, err := NewManager(defaultCgroup2Path, groupPath, &res) 44 | require.NoError(t, err, "failed to init new cgroup manager") 45 | t.Cleanup(func() { 46 | os.Remove(c.path) 47 | }) 48 | 49 | checkFileContent(t, c.path, "io.max", "8:0 rbps=max wbps=max riops=120 wiops=max") 50 | } 51 | -------------------------------------------------------------------------------- /cgroup2/manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "syscall" 24 | "testing" 25 | "time" 26 | 27 | "github.com/opencontainers/runtime-spec/specs-go" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | "go.uber.org/goleak" 31 | ) 32 | 33 | func setupForNewSystemd(t *testing.T) (cmd *exec.Cmd, group string) { 34 | cmd = exec.Command("cat") 35 | err := cmd.Start() 36 | require.NoError(t, err, "failed to start cat process") 37 | proc := cmd.Process 38 | require.NotNil(t, proc, "process was nil") 39 | 40 | group = fmt.Sprintf("testing-watcher-%d.scope", proc.Pid) 41 | 42 | return 43 | } 44 | 45 | func TestErrorsWhenUnitAlreadyExists(t *testing.T) { 46 | checkCgroupMode(t) 47 | 48 | cmd, group := setupForNewSystemd(t) 49 | proc := cmd.Process 50 | 51 | _, err := NewSystemd("", group, proc.Pid, &Resources{}) 52 | require.NoError(t, err, "Failed to init new cgroup manager") 53 | 54 | _, err = NewSystemd("", group, proc.Pid, &Resources{}) 55 | if err == nil { 56 | t.Fatal("Expected recreating cgroup manager should fail") 57 | } else if !isUnitExists(err) { 58 | t.Fatalf("Failed to init cgroup manager with unexpected error: %s", err) 59 | } 60 | } 61 | 62 | // kubelet relies on this behavior to make sure a slice exists 63 | func TestIgnoreUnitExistsWhenPidNegativeOne(t *testing.T) { 64 | checkCgroupMode(t) 65 | 66 | cmd, group := setupForNewSystemd(t) 67 | proc := cmd.Process 68 | 69 | _, err := NewSystemd("", group, proc.Pid, &Resources{}) 70 | require.NoError(t, err, "Failed to init new cgroup manager") 71 | 72 | _, err = NewSystemd("", group, -1, &Resources{}) 73 | require.NoError(t, err, "Expected to be able to recreate cgroup manager") 74 | } 75 | 76 | //nolint:staticcheck // Staticcheck false positives for nil pointer deference after t.Fatal 77 | func TestEventChanCleanupOnCgroupRemoval(t *testing.T) { 78 | checkCgroupMode(t) 79 | 80 | cmd := exec.Command("cat") 81 | stdin, err := cmd.StdinPipe() 82 | require.NoError(t, err, "failed to create cat process") 83 | 84 | err = cmd.Start() 85 | require.NoError(t, err, "failed to start cat process") 86 | 87 | proc := cmd.Process 88 | require.NotNil(t, proc, "process was nil") 89 | 90 | group := fmt.Sprintf("testing-watcher-%d.scope", proc.Pid) 91 | c, err := NewSystemd("", group, proc.Pid, &Resources{}) 92 | require.NoError(t, err, "failed to init new cgroup manager") 93 | 94 | evCh, errCh := c.EventChan() 95 | 96 | // give event goroutine a chance to start 97 | time.Sleep(500 * time.Millisecond) 98 | 99 | err = stdin.Close() 100 | require.NoError(t, err, "failed closing stdin") 101 | 102 | err = cmd.Wait() 103 | require.NoError(t, err, "failed waiting for cmd") 104 | 105 | done := false 106 | for !done { 107 | select { 108 | case <-evCh: 109 | case err := <-errCh: 110 | require.NoError(t, err, "unexpected error on error channel") 111 | done = true 112 | case <-time.After(5 * time.Second): 113 | t.Fatal("Timed out") 114 | } 115 | } 116 | goleak.VerifyNone(t) 117 | } 118 | 119 | func TestSystemdFullPath(t *testing.T) { 120 | tests := []struct { 121 | inputSlice string 122 | inputGroup string 123 | expectedOut string 124 | }{ 125 | { 126 | inputSlice: "user.slice", 127 | inputGroup: "myGroup.slice", 128 | expectedOut: "/sys/fs/cgroup/user.slice/myGroup.slice", 129 | }, 130 | { 131 | inputSlice: "/", 132 | inputGroup: "myGroup.slice", 133 | expectedOut: "/sys/fs/cgroup/myGroup.slice", 134 | }, 135 | { 136 | inputSlice: "system.slice", 137 | inputGroup: "myGroup.slice", 138 | expectedOut: "/sys/fs/cgroup/system.slice/myGroup.slice", 139 | }, 140 | { 141 | inputSlice: "user.slice", 142 | inputGroup: "my-group.slice", 143 | expectedOut: "/sys/fs/cgroup/user.slice/my.slice/my-group.slice", 144 | }, 145 | { 146 | inputSlice: "user.slice", 147 | inputGroup: "my-group-more-dashes.slice", 148 | expectedOut: "/sys/fs/cgroup/user.slice/my.slice/my-group.slice/my-group-more.slice/my-group-more-dashes.slice", 149 | }, 150 | { 151 | inputSlice: "user.slice", 152 | inputGroup: "my-group-dashes.slice", 153 | expectedOut: "/sys/fs/cgroup/user.slice/my.slice/my-group.slice/my-group-dashes.slice", 154 | }, 155 | { 156 | inputSlice: "user.slice", 157 | inputGroup: "myGroup.scope", 158 | expectedOut: "/sys/fs/cgroup/user.slice/myGroup.scope", 159 | }, 160 | { 161 | inputSlice: "user.slice", 162 | inputGroup: "my-group-dashes.scope", 163 | expectedOut: "/sys/fs/cgroup/user.slice/my-group-dashes.scope", 164 | }, 165 | { 166 | inputSlice: "test-waldo.slice", 167 | inputGroup: "my-group.slice", 168 | expectedOut: "/sys/fs/cgroup/test.slice/test-waldo.slice/my.slice/my-group.slice", 169 | }, 170 | { 171 | inputSlice: "test-waldo.slice", 172 | inputGroup: "my.service", 173 | expectedOut: "/sys/fs/cgroup/test.slice/test-waldo.slice/my.service", 174 | }, 175 | } 176 | 177 | for _, test := range tests { 178 | actual := getSystemdFullPath(test.inputSlice, test.inputGroup) 179 | assert.Equal(t, test.expectedOut, actual) 180 | } 181 | } 182 | 183 | func TestKill(t *testing.T) { 184 | checkCgroupMode(t) 185 | manager, err := NewManager(defaultCgroup2Path, "/test1", ToResources(&specs.LinuxResources{})) 186 | require.NoError(t, err) 187 | t.Cleanup(func() { 188 | _ = manager.Delete() 189 | }) 190 | 191 | var ( 192 | procs []*exec.Cmd 193 | numProcs = 5 194 | ) 195 | for i := 0; i < numProcs; i++ { 196 | cmd := exec.Command("sleep", "infinity") 197 | // Don't leak the process if we fail to join the cg, 198 | // send sigkill after tests over. 199 | cmd.SysProcAttr = &syscall.SysProcAttr{ 200 | Pdeathsig: syscall.SIGKILL, 201 | } 202 | err = cmd.Start() 203 | require.NoError(t, err) 204 | 205 | err = manager.AddProc(uint64(cmd.Process.Pid)) 206 | require.NoError(t, err) 207 | 208 | procs = append(procs, cmd) 209 | } 210 | // Verify we have 5 pids before beginning Kill below. 211 | pids, err := manager.Procs(true) 212 | require.NoError(t, err) 213 | require.Len(t, pids, numProcs, "pid count unexpected") 214 | threads, err := manager.Threads(true) 215 | require.NoError(t, err) 216 | require.Len(t, threads, numProcs, "pid count unexpected") 217 | 218 | // Now run kill, and check that nothing is running after. 219 | err = manager.Kill() 220 | require.NoError(t, err) 221 | 222 | done := make(chan struct{}) 223 | go func() { 224 | for _, proc := range procs { 225 | _ = proc.Wait() 226 | } 227 | done <- struct{}{} 228 | }() 229 | 230 | select { 231 | case <-time.After(time.Second * 3): 232 | t.Fatal("timed out waiting for processes to exit") 233 | case <-done: 234 | } 235 | } 236 | 237 | func TestMoveTo(t *testing.T) { 238 | checkCgroupMode(t) 239 | 240 | src, err := NewManager(defaultCgroup2Path, "/test-moveto-src", ToResources(&specs.LinuxResources{})) 241 | require.NoError(t, err) 242 | t.Cleanup(func() { 243 | _ = src.Kill() 244 | _ = src.Delete() 245 | }) 246 | 247 | cmd := exec.Command("sleep", "infinity") 248 | // Don't leak the process if we fail to join the cg, 249 | // send sigkill after tests over. 250 | cmd.SysProcAttr = &syscall.SysProcAttr{ 251 | Pdeathsig: syscall.SIGKILL, 252 | } 253 | err = cmd.Start() 254 | require.NoError(t, err) 255 | 256 | proc := cmd.Process.Pid 257 | err = src.AddProc(uint64(proc)) 258 | require.NoError(t, err) 259 | 260 | destination, err := NewManager(defaultCgroup2Path, "/test-moveto-dest", ToResources(&specs.LinuxResources{})) 261 | require.NoError(t, err) 262 | t.Cleanup(func() { 263 | _ = destination.Kill() 264 | _ = destination.Delete() 265 | }) 266 | 267 | err = src.MoveTo(destination) 268 | require.NoError(t, err) 269 | 270 | desProcs, err := destination.Procs(true) 271 | require.NoError(t, err) 272 | 273 | desMap := make(map[int]bool) 274 | for _, p := range desProcs { 275 | desMap[int(p)] = true 276 | } 277 | if !desMap[proc] { 278 | t.Fatalf("process %v not in destination cgroup", proc) 279 | } 280 | } 281 | 282 | func TestCgroupType(t *testing.T) { 283 | checkCgroupMode(t) 284 | manager, err := NewManager(defaultCgroup2Path, "/test-type", ToResources(&specs.LinuxResources{})) 285 | require.NoError(t, err) 286 | submanager, err := NewManager(defaultCgroup2Path, "/test-type/sub", ToResources(&specs.LinuxResources{})) 287 | require.NoError(t, err) 288 | 289 | t.Cleanup(func() { 290 | _ = submanager.Delete() 291 | _ = manager.Delete() 292 | }) 293 | 294 | // Check initial type is domain 295 | cgType, err := manager.GetType() 296 | require.NoError(t, err) 297 | require.Equal(t, cgType, Domain) 298 | 299 | // Swap sub cgroup to threaded 300 | require.NoError(t, submanager.SetType(Threaded)) 301 | 302 | // Check sub cgroup type is threaded 303 | cgType, err = submanager.GetType() 304 | require.NoError(t, err) 305 | require.Equal(t, cgType, Threaded) 306 | 307 | // Check parent cgroup type is domain threaded 308 | cgType, err = manager.GetType() 309 | require.NoError(t, err) 310 | require.Equal(t, cgType, DomainThreaded) 311 | } 312 | 313 | func TestCgroupv2PSIStats(t *testing.T) { 314 | checkCgroupMode(t) 315 | group := "/psi-test-cg" 316 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 317 | res := Resources{} 318 | c, err := NewManager(defaultCgroup2Path, groupPath, &res) 319 | require.NoError(t, err, "failed to init new cgroup manager") 320 | t.Cleanup(func() { 321 | os.Remove(c.path) 322 | }) 323 | 324 | stats, err := c.Stat() 325 | require.NoError(t, err, "failed to get cgroup stats") 326 | if stats.CPU.PSI == nil || stats.Memory.PSI == nil || stats.Io.PSI == nil { 327 | t.Error("expected psi not nil but got nil") 328 | } 329 | } 330 | 331 | func TestSystemdCgroupPSIController(t *testing.T) { 332 | checkCgroupMode(t) 333 | group := fmt.Sprintf("testing-psi-%d.scope", os.Getpid()) 334 | pid := os.Getpid() 335 | res := Resources{} 336 | c, err := NewSystemd("", group, pid, &res) 337 | require.NoError(t, err, "failed to init new cgroup systemd manager") 338 | 339 | stats, err := c.Stat() 340 | require.NoError(t, err, "failed to get cgroup stats") 341 | if stats.CPU.PSI == nil || stats.Memory.PSI == nil || stats.Io.PSI == nil { 342 | t.Error("expected psi not nil but got nil") 343 | } 344 | } 345 | 346 | func BenchmarkStat(b *testing.B) { 347 | checkCgroupMode(b) 348 | group := "/stat-test-cg" 349 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 350 | c, err := NewManager(defaultCgroup2Path, groupPath, &Resources{}) 351 | require.NoErrorf(b, err, "failed to init new cgroup manager") 352 | b.Cleanup(func() { 353 | _ = c.Delete() 354 | }) 355 | 356 | b.ReportAllocs() 357 | for i := 0; i < b.N; i++ { 358 | _, err := c.Stat() 359 | require.NoError(b, err) 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /cgroup2/memory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | type Memory struct { 20 | Swap *int64 21 | Min *int64 22 | Max *int64 23 | Low *int64 24 | High *int64 25 | } 26 | 27 | func (r *Memory) Values() (o []Value) { 28 | if r.Swap != nil { 29 | o = append(o, Value{ 30 | filename: "memory.swap.max", 31 | value: *r.Swap, 32 | }) 33 | } 34 | if r.Min != nil { 35 | o = append(o, Value{ 36 | filename: "memory.min", 37 | value: *r.Min, 38 | }) 39 | } 40 | if r.Max != nil { 41 | o = append(o, Value{ 42 | filename: "memory.max", 43 | value: *r.Max, 44 | }) 45 | } 46 | if r.Low != nil { 47 | o = append(o, Value{ 48 | filename: "memory.low", 49 | value: *r.Low, 50 | }) 51 | } 52 | if r.High != nil { 53 | o = append(o, Value{ 54 | filename: "memory.high", 55 | value: *r.High, 56 | }) 57 | } 58 | return o 59 | } 60 | -------------------------------------------------------------------------------- /cgroup2/memoryv2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestCgroupv2MemoryStats(t *testing.T) { 29 | checkCgroupMode(t) 30 | group := "/memory-test-cg" 31 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 32 | res := Resources{ 33 | Memory: &Memory{ 34 | Max: pointerInt64(629145600), 35 | Swap: pointerInt64(314572800), 36 | High: pointerInt64(524288000), 37 | }, 38 | } 39 | c, err := NewManager(defaultCgroup2Path, groupPath, &res) 40 | require.NoError(t, err, "failed to init new cgroup manager") 41 | t.Cleanup(func() { 42 | os.Remove(c.path) 43 | }) 44 | 45 | stats, err := c.Stat() 46 | require.NoError(t, err, "failed to get cgroup stats") 47 | 48 | assert.Equal(t, uint64(314572800), stats.Memory.SwapLimit) 49 | assert.Equal(t, uint64(629145600), stats.Memory.UsageLimit) 50 | checkFileContent(t, c.path, "memory.swap.max", "314572800") 51 | checkFileContent(t, c.path, "memory.max", "629145600") 52 | } 53 | 54 | func TestSystemdCgroupMemoryController(t *testing.T) { 55 | checkCgroupMode(t) 56 | group := fmt.Sprintf("testing-memory-%d.scope", os.Getpid()) 57 | res := Resources{ 58 | Memory: &Memory{ 59 | Min: pointerInt64(16384), 60 | Max: pointerInt64(629145600), 61 | }, 62 | } 63 | c, err := NewSystemd("", group, os.Getpid(), &res) 64 | require.NoError(t, err, "failed to init new cgroup systemd manager") 65 | 66 | checkFileContent(t, c.path, "memory.min", "16384") 67 | checkFileContent(t, c.path, "memory.max", "629145600") 68 | } 69 | -------------------------------------------------------------------------------- /cgroup2/paths.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "strings" 23 | ) 24 | 25 | // NestedGroupPath will nest the cgroups based on the calling processes cgroup 26 | // placing its child processes inside its own path 27 | func NestedGroupPath(suffix string) (string, error) { 28 | path, err := parseCgroupFile("/proc/self/cgroup") 29 | if err != nil { 30 | return "", err 31 | } 32 | return filepath.Join(path, suffix), nil 33 | } 34 | 35 | // PidGroupPath will return the correct cgroup paths for an existing process running inside a cgroup 36 | // This is commonly used for the Load function to restore an existing container 37 | func PidGroupPath(pid int) (string, error) { 38 | p := fmt.Sprintf("/proc/%d/cgroup", pid) 39 | return parseCgroupFile(p) 40 | } 41 | 42 | // VerifyGroupPath verifies the format of group path string g. 43 | // The format is same as the third field in /proc/PID/cgroup. 44 | // e.g. "/user.slice/user-1001.slice/session-1.scope" 45 | // 46 | // g must be a "clean" absolute path starts with "/", and must not contain "/sys/fs/cgroup" prefix. 47 | // 48 | // VerifyGroupPath doesn't verify whether g actually exists on the system. 49 | func VerifyGroupPath(g string) error { 50 | if !strings.HasPrefix(g, "/") { 51 | return ErrInvalidGroupPath 52 | } 53 | if filepath.Clean(g) != g { 54 | return ErrInvalidGroupPath 55 | } 56 | if strings.HasPrefix(g, "/sys/fs/cgroup") { 57 | return ErrInvalidGroupPath 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /cgroup2/paths_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestVerifyGroupPath(t *testing.T) { 26 | valids := map[string]bool{ 27 | "/": true, 28 | "": false, 29 | "/foo": true, 30 | "/foo/bar": true, 31 | "/sys/fs/cgroup/foo": false, 32 | "/sys/fs/cgroup/unified/foo": false, 33 | "foo": false, 34 | "/foo/../bar": false, 35 | } 36 | for s, valid := range valids { 37 | err := VerifyGroupPath(s) 38 | if valid { 39 | assert.NoError(t, err) 40 | } else { 41 | assert.Error(t, err) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cgroup2/pids.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import "strconv" 20 | 21 | type Pids struct { 22 | Max int64 23 | } 24 | 25 | func (r *Pids) Values() (o []Value) { 26 | if r.Max != 0 { 27 | limit := "max" 28 | if r.Max > 0 { 29 | limit = strconv.FormatInt(r.Max, 10) 30 | } 31 | o = append(o, Value{ 32 | filename: "pids.max", 33 | value: limit, 34 | }) 35 | } 36 | return o 37 | } 38 | -------------------------------------------------------------------------------- /cgroup2/pidsv2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strconv" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestCgroupv2PidsStats(t *testing.T) { 29 | checkCgroupMode(t) 30 | group := "/pids-test-cg" 31 | groupPath := fmt.Sprintf("%s-%d", group, os.Getpid()) 32 | var max int64 = 1000 33 | res := Resources{ 34 | Pids: &Pids{ 35 | Max: max, 36 | }, 37 | } 38 | c, err := NewManager(defaultCgroup2Path, groupPath, &res) 39 | require.NoError(t, err, "failed to init new cgroup manager") 40 | t.Cleanup(func() { 41 | os.Remove(c.path) 42 | }) 43 | 44 | checkFileContent(t, c.path, "pids.max", strconv.Itoa(int(max))) 45 | } 46 | 47 | func TestSystemdCgroupPidsController(t *testing.T) { 48 | checkCgroupMode(t) 49 | group := fmt.Sprintf("testing-pids-%d.scope", os.Getpid()) 50 | pid := os.Getpid() 51 | res := Resources{} 52 | c, err := NewSystemd("", group, pid, &res) 53 | require.NoError(t, err, "failed to init new cgroup systemd manager") 54 | 55 | checkFileContent(t, c.path, "cgroup.procs", strconv.Itoa(pid)) 56 | } 57 | -------------------------------------------------------------------------------- /cgroup2/rdma.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | type RDMA struct { 24 | Limit []RDMAEntry 25 | } 26 | 27 | type RDMAEntry struct { 28 | Device string 29 | HcaHandles uint32 30 | HcaObjects uint32 31 | } 32 | 33 | func (r RDMAEntry) String() string { 34 | return fmt.Sprintf("%s hca_handle=%d hca_object=%d", r.Device, r.HcaHandles, r.HcaObjects) 35 | } 36 | 37 | func (r *RDMA) Values() (o []Value) { 38 | for _, e := range r.Limit { 39 | o = append(o, Value{ 40 | filename: "rdma.max", 41 | value: e.String(), 42 | }) 43 | } 44 | 45 | return o 46 | } 47 | -------------------------------------------------------------------------------- /cgroup2/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | ) 24 | 25 | // State is a type that represents the state of the current cgroup 26 | type State string 27 | 28 | const ( 29 | Unknown State = "" 30 | Thawed State = "thawed" 31 | Frozen State = "frozen" 32 | Deleted State = "deleted" 33 | 34 | cgroupFreeze = "cgroup.freeze" 35 | ) 36 | 37 | func (s State) Values() []Value { 38 | v := Value{ 39 | filename: cgroupFreeze, 40 | } 41 | switch s { 42 | case Frozen: 43 | v.value = "1" 44 | case Thawed: 45 | v.value = "0" 46 | } 47 | return []Value{ 48 | v, 49 | } 50 | } 51 | 52 | func fetchState(path string) (State, error) { 53 | current, err := os.ReadFile(filepath.Join(path, cgroupFreeze)) 54 | if err != nil { 55 | return Unknown, err 56 | } 57 | switch strings.TrimSpace(string(current)) { 58 | case "1": 59 | return Frozen, nil 60 | case "0": 61 | return Thawed, nil 62 | default: 63 | return Unknown, nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cgroup2/stats/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package stats 18 | -------------------------------------------------------------------------------- /cgroup2/stats/metrics.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package io.containerd.cgroups.v2; 4 | 5 | option go_package = "github.com/containerd/cgroups/cgroup2/stats"; 6 | 7 | message Metrics { 8 | PidsStat pids = 1; 9 | CPUStat cpu = 2; 10 | MemoryStat memory = 4; 11 | RdmaStat rdma = 5; 12 | IOStat io = 6; 13 | repeated HugeTlbStat hugetlb = 7; 14 | MemoryEvents memory_events = 8; 15 | repeated NetworkStat network = 9; 16 | } 17 | 18 | message PSIData { 19 | double avg10 = 1; 20 | double avg60 = 2; 21 | double avg300 = 3; 22 | uint64 total = 4; 23 | } 24 | 25 | message PSIStats { 26 | PSIData some = 1; 27 | PSIData full = 2; 28 | } 29 | 30 | message PidsStat { 31 | uint64 current = 1; 32 | uint64 limit = 2; 33 | } 34 | 35 | message CPUStat { 36 | uint64 usage_usec = 1; 37 | uint64 user_usec = 2; 38 | uint64 system_usec = 3; 39 | uint64 nr_periods = 4; 40 | uint64 nr_throttled = 5; 41 | uint64 throttled_usec = 6; 42 | PSIStats psi = 7; 43 | } 44 | 45 | message MemoryStat { 46 | uint64 anon = 1; 47 | uint64 file = 2; 48 | uint64 kernel_stack = 3; 49 | uint64 slab = 4; 50 | uint64 sock = 5; 51 | uint64 shmem = 6; 52 | uint64 file_mapped = 7; 53 | uint64 file_dirty = 8; 54 | uint64 file_writeback = 9; 55 | uint64 anon_thp = 10; 56 | uint64 inactive_anon = 11; 57 | uint64 active_anon = 12; 58 | uint64 inactive_file = 13; 59 | uint64 active_file = 14; 60 | uint64 unevictable = 15; 61 | uint64 slab_reclaimable = 16; 62 | uint64 slab_unreclaimable = 17; 63 | uint64 pgfault = 18; 64 | uint64 pgmajfault = 19; 65 | uint64 workingset_refault = 20; 66 | uint64 workingset_activate = 21; 67 | uint64 workingset_nodereclaim = 22; 68 | uint64 pgrefill = 23; 69 | uint64 pgscan = 24; 70 | uint64 pgsteal = 25; 71 | uint64 pgactivate = 26; 72 | uint64 pgdeactivate = 27; 73 | uint64 pglazyfree = 28; 74 | uint64 pglazyfreed = 29; 75 | uint64 thp_fault_alloc = 30; 76 | uint64 thp_collapse_alloc = 31; 77 | uint64 usage = 32; 78 | uint64 usage_limit = 33; 79 | uint64 swap_usage = 34; 80 | uint64 swap_limit = 35; 81 | uint64 max_usage = 36; 82 | uint64 swap_max_usage = 37; 83 | PSIStats psi = 38; 84 | } 85 | 86 | message MemoryEvents { 87 | uint64 low = 1; 88 | uint64 high = 2; 89 | uint64 max = 3; 90 | uint64 oom = 4; 91 | uint64 oom_kill = 5; 92 | } 93 | 94 | message RdmaStat { 95 | repeated RdmaEntry current = 1; 96 | repeated RdmaEntry limit = 2; 97 | } 98 | 99 | message RdmaEntry { 100 | string device = 1; 101 | uint32 hca_handles = 2; 102 | uint32 hca_objects = 3; 103 | } 104 | 105 | message IOStat { 106 | repeated IOEntry usage = 1; 107 | PSIStats psi = 2; 108 | } 109 | 110 | message IOEntry { 111 | uint64 major = 1; 112 | uint64 minor = 2; 113 | uint64 rbytes = 3; 114 | uint64 wbytes = 4; 115 | uint64 rios = 5; 116 | uint64 wios = 6; 117 | } 118 | 119 | message HugeTlbStat { 120 | uint64 current = 1; 121 | uint64 max = 2; 122 | string pagesize = 3; 123 | } 124 | 125 | message NetworkStat { 126 | string name = 1; 127 | uint64 rx_bytes = 2; 128 | uint64 rx_packets = 3; 129 | uint64 rx_errors = 4; 130 | uint64 rx_dropped = 5; 131 | uint64 tx_bytes = 6; 132 | uint64 tx_packets = 7; 133 | uint64 tx_errors = 8; 134 | uint64 tx_dropped = 9; 135 | } -------------------------------------------------------------------------------- /cgroup2/testutils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | "golang.org/x/sys/unix" 28 | ) 29 | 30 | func checkCgroupMode(tb testing.TB) { 31 | var st unix.Statfs_t 32 | err := unix.Statfs(defaultCgroup2Path, &st) 33 | require.NoError(tb, err, "cannot statfs cgroup root") 34 | 35 | isUnified := st.Type == unix.CGROUP2_SUPER_MAGIC 36 | if !isUnified { 37 | tb.Skip("System running in hybrid or cgroupv1 mode") 38 | } 39 | } 40 | 41 | func checkCgroupControllerSupported(t *testing.T, controller string) { 42 | b, err := os.ReadFile(filepath.Join(defaultCgroup2Path, controllersFile)) 43 | if err != nil || !strings.Contains(string(b), controller) { 44 | t.Skipf("Controller: %s is not supported on that system", controller) 45 | } 46 | } 47 | 48 | func checkFileContent(t *testing.T, path, filename, value string) { 49 | out, err := os.ReadFile(filepath.Join(path, filename)) 50 | require.NoErrorf(t, err, "failed to read %s file", filename) 51 | assert.Equal(t, value, strings.TrimSpace(string(out))) 52 | } 53 | -------------------------------------------------------------------------------- /cgroup2/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroup2 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/containerd/cgroups/v3/cgroup2/stats" 26 | 27 | "github.com/opencontainers/runtime-spec/specs-go" 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestParseCgroupFromReader(t *testing.T) { 32 | cases := map[string]string{ 33 | "0::/user.slice/user-1001.slice/session-1.scope\n": "/user.slice/user-1001.slice/session-1.scope", 34 | "2:cpuset:/foo\n1:name=systemd:/\n": "", 35 | "2:cpuset:/foo\n1:name=systemd:/\n0::/user.slice/user-1001.slice/session-1.scope\n": "/user.slice/user-1001.slice/session-1.scope", 36 | } 37 | for s, expected := range cases { 38 | g, err := parseCgroupFromReader(strings.NewReader(s)) 39 | if expected != "" { 40 | assert.Equal(t, g, expected) 41 | assert.NoError(t, err) 42 | } else { 43 | assert.Error(t, err) 44 | } 45 | } 46 | } 47 | 48 | func TestParseStatCPUPSI(t *testing.T) { 49 | const examplePSIData = `some avg10=1.71 avg60=2.36 avg300=2.57 total=230548833 50 | full avg10=1.00 avg60=1.01 avg300=1.00 total=157622356` 51 | 52 | fakeCgroupDir := t.TempDir() 53 | statPath := filepath.Join(fakeCgroupDir, "cpu.pressure") 54 | 55 | if err := os.WriteFile(statPath, []byte(examplePSIData), 0o644); err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | st := getStatPSIFromFile(filepath.Join(fakeCgroupDir, "cpu.pressure")) 60 | expected := stats.PSIStats{ 61 | Some: &stats.PSIData{ 62 | Avg10: 1.71, 63 | Avg60: 2.36, 64 | Avg300: 2.57, 65 | Total: 230548833, 66 | }, 67 | Full: &stats.PSIData{ 68 | Avg10: 1.00, 69 | Avg60: 1.01, 70 | Avg300: 1.00, 71 | Total: 157622356, 72 | }, 73 | } 74 | assert.Equal(t, &st.Some, &expected.Some) 75 | assert.Equal(t, &st.Full, &expected.Full) 76 | } 77 | 78 | func TestToResources(t *testing.T) { 79 | var ( 80 | quota int64 = 8000 81 | period uint64 = 10000 82 | shares uint64 = 5000 83 | 84 | mem int64 = 300 85 | swap int64 = 500 86 | ) 87 | weight := 1 + ((shares-2)*9999)/262142 88 | res := specs.LinuxResources{ 89 | CPU: &specs.LinuxCPU{Quota: "a, Period: &period, Shares: &shares}, 90 | Memory: &specs.LinuxMemory{Limit: &mem, Swap: &swap}, 91 | } 92 | v2resources := ToResources(&res) 93 | 94 | assert.Equal(t, weight, *v2resources.CPU.Weight) 95 | assert.Equal(t, CPUMax("8000 10000"), v2resources.CPU.Max) 96 | assert.Equal(t, swap-mem, *v2resources.Memory.Swap) 97 | 98 | res2 := specs.LinuxResources{CPU: &specs.LinuxCPU{Period: &period}} 99 | v2resources2 := ToResources(&res2) 100 | assert.Equal(t, CPUMax("max 10000"), v2resources2.CPU.Max) 101 | } 102 | 103 | func BenchmarkGetStatFileContentUint64(b *testing.B) { 104 | b.ReportAllocs() 105 | 106 | for i := 0; i < b.N; i++ { 107 | _ = getStatFileContentUint64("/proc/self/loginuid") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /cmd/cgctl/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "strconv" 24 | 25 | "github.com/containerd/cgroups/v3" 26 | "github.com/containerd/cgroups/v3/cgroup2" 27 | "github.com/containerd/log" 28 | "github.com/urfave/cli" 29 | ) 30 | 31 | func main() { 32 | app := cli.NewApp() 33 | app.Name = "cgctl" 34 | app.Version = "1" 35 | app.Usage = "cgroup v2 management tool" 36 | app.Flags = []cli.Flag{ 37 | cli.BoolFlag{ 38 | Name: "debug", 39 | Usage: "enable debug output in the logs", 40 | }, 41 | cli.StringFlag{ 42 | Name: "mountpoint", 43 | Usage: "cgroup mountpoint", 44 | Value: "/sys/fs/cgroup", 45 | }, 46 | } 47 | app.Commands = []cli.Command{ 48 | modeCommand, 49 | newCommand, 50 | delCommand, 51 | listCommand, 52 | listControllersCommand, 53 | statCommand, 54 | newSystemdCommand, 55 | deleteSystemdCommand, 56 | } 57 | app.Before = func(clix *cli.Context) error { 58 | if clix.GlobalBool("debug") { 59 | _ = log.SetLevel("debug") 60 | } 61 | return nil 62 | } 63 | if err := app.Run(os.Args); err != nil { 64 | fmt.Fprintln(os.Stderr, err) 65 | os.Exit(1) 66 | } 67 | } 68 | 69 | var newCommand = cli.Command{ 70 | Name: "new", 71 | Usage: "create a new cgroup", 72 | Flags: []cli.Flag{ 73 | cli.BoolFlag{ 74 | Name: "enable", 75 | Usage: "enable the controllers for the group", 76 | }, 77 | }, 78 | Action: func(clix *cli.Context) error { 79 | path := clix.Args().First() 80 | c, err := cgroup2.NewManager(clix.GlobalString("mountpoint"), path, &cgroup2.Resources{}) 81 | if err != nil { 82 | return err 83 | } 84 | if clix.Bool("enable") { 85 | controllers, err := c.RootControllers() 86 | if err != nil { 87 | return err 88 | } 89 | if err := c.ToggleControllers(controllers, cgroup2.Enable); err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | }, 95 | } 96 | 97 | var delCommand = cli.Command{ 98 | Name: "del", 99 | Usage: "delete a cgroup", 100 | Action: func(clix *cli.Context) error { 101 | path := clix.Args().First() 102 | c, err := cgroup2.Load(path, cgroup2.WithMountpoint(clix.GlobalString("mountpoint"))) 103 | if err != nil { 104 | return err 105 | } 106 | return c.Delete() 107 | }, 108 | } 109 | 110 | var listCommand = cli.Command{ 111 | Name: "list", 112 | Usage: "list processes in a cgroup", 113 | Action: func(clix *cli.Context) error { 114 | path := clix.Args().First() 115 | c, err := cgroup2.Load(path, cgroup2.WithMountpoint(clix.GlobalString("mountpoint"))) 116 | if err != nil { 117 | return err 118 | } 119 | procs, err := c.Procs(true) 120 | if err != nil { 121 | return err 122 | } 123 | for _, p := range procs { 124 | fmt.Println(p) 125 | } 126 | return nil 127 | }, 128 | } 129 | 130 | var listControllersCommand = cli.Command{ 131 | Name: "list-controllers", 132 | Usage: "list controllers in a cgroup", 133 | Action: func(clix *cli.Context) error { 134 | path := clix.Args().First() 135 | c, err := cgroup2.Load(path, cgroup2.WithMountpoint(clix.GlobalString("mountpoint"))) 136 | if err != nil { 137 | return err 138 | } 139 | controllers, err := c.Controllers() 140 | if err != nil { 141 | return err 142 | } 143 | for _, c := range controllers { 144 | fmt.Println(c) 145 | } 146 | return nil 147 | }, 148 | } 149 | 150 | var statCommand = cli.Command{ 151 | Name: "stat", 152 | Usage: "stat a cgroup", 153 | Action: func(clix *cli.Context) error { 154 | path := clix.Args().First() 155 | c, err := cgroup2.Load(path, cgroup2.WithMountpoint(clix.GlobalString("mountpoint"))) 156 | if err != nil { 157 | return err 158 | } 159 | stats, err := c.Stat() 160 | if err != nil { 161 | return err 162 | } 163 | return json.NewEncoder(os.Stdout).Encode(stats) 164 | }, 165 | } 166 | 167 | var newSystemdCommand = cli.Command{ 168 | Name: "systemd", 169 | Usage: "create a new systemd managed cgroup", 170 | Action: func(clix *cli.Context) error { 171 | path := clix.Args().First() 172 | pidStr := clix.Args().Get(1) 173 | pid := os.Getpid() 174 | if pidStr != "" { 175 | pid, _ = strconv.Atoi(pidStr) 176 | } 177 | 178 | _, err := cgroup2.NewSystemd("", path, pid, &cgroup2.Resources{}) 179 | if err != nil { 180 | return err 181 | } 182 | return nil 183 | }, 184 | } 185 | 186 | var deleteSystemdCommand = cli.Command{ 187 | Name: "del-systemd", 188 | Usage: "delete a systemd managed cgroup", 189 | Action: func(clix *cli.Context) error { 190 | path := clix.Args().First() 191 | m, err := cgroup2.LoadSystemd("", path) 192 | if err != nil { 193 | return err 194 | } 195 | err = m.DeleteSystemd() 196 | if err != nil { 197 | return err 198 | } 199 | return nil 200 | }, 201 | } 202 | 203 | var modeCommand = cli.Command{ 204 | Name: "mode", 205 | Usage: "return the cgroup mode that is mounted on the system", 206 | Action: func(clix *cli.Context) error { 207 | mode := cgroups.Mode() 208 | switch mode { 209 | case cgroups.Legacy: 210 | fmt.Println("legacy") 211 | case cgroups.Hybrid: 212 | fmt.Println("hybrid") 213 | case cgroups.Unified: 214 | fmt.Println("unified") 215 | case cgroups.Unavailable: 216 | fmt.Println("cgroups unavailable") 217 | } 218 | return nil 219 | }, 220 | } 221 | -------------------------------------------------------------------------------- /cmd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerd/cgroups/cmd 2 | 3 | go 1.22.0 4 | 5 | replace github.com/containerd/cgroups/v3 => ../ 6 | 7 | require ( 8 | github.com/containerd/cgroups/v3 v3.0.0-00010101000000-000000000000 9 | github.com/containerd/log v0.1.0 10 | github.com/urfave/cli v1.22.16 11 | ) 12 | 13 | require ( 14 | github.com/cilium/ebpf v0.16.0 // indirect 15 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 16 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 17 | github.com/godbus/dbus/v5 v5.1.0 // indirect 18 | github.com/moby/sys/userns v0.1.0 // indirect 19 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 20 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 21 | github.com/sirupsen/logrus v1.9.3 // indirect 22 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect 23 | golang.org/x/sys v0.27.0 // indirect 24 | google.golang.org/protobuf v1.35.2 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /cmd/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 2 | github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= 3 | github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= 4 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 5 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 6 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 7 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 14 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 15 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 16 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 17 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 21 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 22 | github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= 23 | github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= 24 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 25 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 26 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 27 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 28 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 29 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 30 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 31 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 32 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 33 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 34 | github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= 35 | github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 39 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 40 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 43 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 46 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 47 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 51 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 52 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 53 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 54 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 55 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 56 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 57 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 58 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= 59 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= 60 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 61 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 62 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 63 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 64 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 66 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 67 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 68 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 71 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerd/cgroups/v3 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.16.0 7 | github.com/containerd/log v0.1.0 8 | github.com/coreos/go-systemd/v22 v22.5.0 9 | github.com/docker/go-units v0.5.0 10 | github.com/godbus/dbus/v5 v5.1.0 11 | github.com/moby/sys/userns v0.1.0 12 | github.com/opencontainers/runtime-spec v1.2.0 13 | github.com/stretchr/testify v1.8.4 14 | go.uber.org/goleak v1.1.12 15 | golang.org/x/sys v0.27.0 16 | google.golang.org/protobuf v1.35.2 17 | ) 18 | 19 | require ( 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/sirupsen/logrus v1.9.3 // indirect 23 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= 2 | github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= 3 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 4 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 5 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 6 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 11 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 12 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 13 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 14 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 15 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 16 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 17 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 18 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 19 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 20 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 21 | github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= 22 | github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 25 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 31 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 32 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 33 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 34 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 35 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 36 | github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= 37 | github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 41 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 42 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 43 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 47 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 48 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 49 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 50 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 51 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 53 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= 54 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= 55 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 56 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 57 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 58 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 62 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 63 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 64 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 67 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 68 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 75 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 76 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 81 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 82 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 83 | golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 84 | golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 85 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 88 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 89 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 91 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 95 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroups 18 | 19 | import ( 20 | "bufio" 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | "sync" 27 | 28 | "github.com/moby/sys/userns" 29 | "golang.org/x/sys/unix" 30 | ) 31 | 32 | var ( 33 | checkMode sync.Once 34 | cgMode CGMode 35 | ) 36 | 37 | const unifiedMountpoint = "/sys/fs/cgroup" 38 | 39 | // CGMode is the cgroups mode of the host system 40 | type CGMode int 41 | 42 | const ( 43 | // Unavailable cgroup mountpoint 44 | Unavailable CGMode = iota 45 | // Legacy cgroups v1 46 | Legacy 47 | // Hybrid with cgroups v1 and v2 controllers mounted 48 | Hybrid 49 | // Unified with only cgroups v2 mounted 50 | Unified 51 | ) 52 | 53 | // Mode returns the cgroups mode running on the host 54 | func Mode() CGMode { 55 | checkMode.Do(func() { 56 | var st unix.Statfs_t 57 | if err := unix.Statfs(unifiedMountpoint, &st); err != nil { 58 | cgMode = Unavailable 59 | return 60 | } 61 | switch st.Type { 62 | case unix.CGROUP2_SUPER_MAGIC: 63 | cgMode = Unified 64 | default: 65 | cgMode = Legacy 66 | if err := unix.Statfs(filepath.Join(unifiedMountpoint, "unified"), &st); err != nil { 67 | return 68 | } 69 | if st.Type == unix.CGROUP2_SUPER_MAGIC { 70 | cgMode = Hybrid 71 | } 72 | } 73 | }) 74 | return cgMode 75 | } 76 | 77 | // RunningInUserNS detects whether we are currently running in a user namespace. 78 | // Copied from github.com/lxc/lxd/shared/util.go 79 | // 80 | // Deprecated: use [userns.RunningInUserNS]. 81 | func RunningInUserNS() bool { 82 | return userns.RunningInUserNS() 83 | } 84 | 85 | // ParseCgroupFileUnified returns legacy subsystem paths as the first value, 86 | // and returns the unified path as the second value. 87 | func ParseCgroupFileUnified(path string) (map[string]string, string, error) { 88 | f, err := os.Open(path) 89 | if err != nil { 90 | return nil, "", err 91 | } 92 | defer f.Close() 93 | return ParseCgroupFromReaderUnified(f) 94 | } 95 | 96 | // ParseCgroupFromReaderUnified returns legacy subsystem paths as the first value, 97 | // and returns the unified path as the second value. 98 | func ParseCgroupFromReaderUnified(r io.Reader) (map[string]string, string, error) { 99 | var ( 100 | cgroups = make(map[string]string) 101 | unified = "" 102 | s = bufio.NewScanner(r) 103 | ) 104 | for s.Scan() { 105 | var ( 106 | text = s.Text() 107 | parts = strings.SplitN(text, ":", 3) 108 | ) 109 | if len(parts) < 3 { 110 | return nil, unified, fmt.Errorf("invalid cgroup entry: %q", text) 111 | } 112 | for _, subs := range strings.Split(parts[1], ",") { 113 | if subs == "" { 114 | unified = parts[2] 115 | } else { 116 | cgroups[subs] = parts[2] 117 | } 118 | } 119 | } 120 | if err := s.Err(); err != nil { 121 | return nil, unified, err 122 | } 123 | return cgroups, unified, nil 124 | } 125 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cgroups 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestParseCgroupFromReaderUnified(t *testing.T) { 25 | const data = `10:devices:/user.slice 26 | 9:net_cls,net_prio:/ 27 | 8:blkio:/ 28 | 7:freezer:/ 29 | 6:perf_event:/ 30 | 5:cpuset:/ 31 | 4:memory:/ 32 | 3:pids:/user.slice/user-1000.slice/user@1000.service 33 | 2:cpu,cpuacct:/ 34 | 1:name=systemd:/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service 35 | 0::/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service` 36 | r := strings.NewReader(data) 37 | paths, unified, err := ParseCgroupFromReaderUnified(r) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | for subsystem, path := range paths { 42 | if subsystem == "" { 43 | t.Fatalf("empty subsystem for %q", path) 44 | } 45 | } 46 | unifiedExpected := "/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service" 47 | if unified != unifiedExpected { 48 | t.Fatalf("expected %q, got %q", unifiedExpected, unified) 49 | } 50 | } 51 | --------------------------------------------------------------------------------