1 | // Licensed to the .NET Foundation under one or more agreements. |
2 | // The .NET Foundation licenses this file to you under the MIT license. |
3 | // See the LICENSE file in the project root for more information. |
4 | |
5 | /*++ |
6 | |
7 | Module Name: |
8 | |
9 | cgroup.cpp |
10 | |
11 | Abstract: |
12 | Read memory and cpu limits for the current process |
13 | --*/ |
14 | |
15 | #include "pal/dbgmsg.h" |
16 | SET_DEFAULT_DEBUG_CHANNEL(MISC); |
17 | #include "pal/palinternal.h" |
18 | #include <sys/resource.h> |
19 | #include "pal/virtual.h" |
20 | #include "pal/cgroup.h" |
21 | #include <algorithm> |
22 | |
23 | #define PROC_MOUNTINFO_FILENAME "/proc/self/mountinfo" |
24 | #define PROC_CGROUP_FILENAME "/proc/self/cgroup" |
25 | #define PROC_STATM_FILENAME "/proc/self/statm" |
26 | #define MEM_LIMIT_FILENAME "/memory.limit_in_bytes" |
27 | #define MEM_USAGE_FILENAME "/memory.usage_in_bytes" |
28 | #define CFS_QUOTA_FILENAME "/cpu.cfs_quota_us" |
29 | #define CFS_PERIOD_FILENAME "/cpu.cfs_period_us" |
30 | class CGroup |
31 | { |
32 | static char *s_memory_cgroup_path; |
33 | static char *s_cpu_cgroup_path; |
34 | public: |
35 | static void Initialize() |
36 | { |
37 | s_memory_cgroup_path = FindCgroupPath(&IsMemorySubsystem); |
38 | s_cpu_cgroup_path = FindCgroupPath(&IsCpuSubsystem); |
39 | } |
40 | |
41 | static void Cleanup() |
42 | { |
43 | PAL_free(s_memory_cgroup_path); |
44 | PAL_free(s_cpu_cgroup_path); |
45 | } |
46 | |
47 | static bool GetPhysicalMemoryLimit(size_t *val) |
48 | { |
49 | char *mem_limit_filename = nullptr; |
50 | bool result = false; |
51 | |
52 | if (s_memory_cgroup_path == nullptr) |
53 | return result; |
54 | |
55 | size_t len = strlen(s_memory_cgroup_path); |
56 | len += strlen(MEM_LIMIT_FILENAME); |
57 | mem_limit_filename = (char*)PAL_malloc(len+1); |
58 | if (mem_limit_filename == nullptr) |
59 | return result; |
60 | |
61 | strcpy_s(mem_limit_filename, len+1, s_memory_cgroup_path); |
62 | strcat_s(mem_limit_filename, len+1, MEM_LIMIT_FILENAME); |
63 | result = ReadMemoryValueFromFile(mem_limit_filename, val); |
64 | PAL_free(mem_limit_filename); |
65 | return result; |
66 | } |
67 | |
68 | static bool GetPhysicalMemoryUsage(size_t *val) |
69 | { |
70 | char *mem_usage_filename = nullptr; |
71 | bool result = false; |
72 | |
73 | if (s_memory_cgroup_path == nullptr) |
74 | return result; |
75 | |
76 | size_t len = strlen(s_memory_cgroup_path); |
77 | len += strlen(MEM_USAGE_FILENAME); |
78 | mem_usage_filename = (char*)malloc(len+1); |
79 | if (mem_usage_filename == nullptr) |
80 | return result; |
81 | |
82 | strcpy(mem_usage_filename, s_memory_cgroup_path); |
83 | strcat(mem_usage_filename, MEM_USAGE_FILENAME); |
84 | result = ReadMemoryValueFromFile(mem_usage_filename, val); |
85 | free(mem_usage_filename); |
86 | return result; |
87 | } |
88 | |
89 | static bool GetCpuLimit(UINT *val) |
90 | { |
91 | long long quota; |
92 | long long period; |
93 | long long cpu_count; |
94 | |
95 | quota = ReadCpuCGroupValue(CFS_QUOTA_FILENAME); |
96 | if (quota <= 0) |
97 | return false; |
98 | |
99 | period = ReadCpuCGroupValue(CFS_PERIOD_FILENAME); |
100 | if (period <= 0) |
101 | return false; |
102 | |
103 | // Cannot have less than 1 CPU |
104 | if (quota <= period) |
105 | { |
106 | *val = 1; |
107 | return true; |
108 | } |
109 | |
110 | cpu_count = quota / period; |
111 | if (cpu_count < UINT_MAX) |
112 | { |
113 | *val = cpu_count; |
114 | } |
115 | else |
116 | { |
117 | *val = UINT_MAX; |
118 | } |
119 | |
120 | return true; |
121 | } |
122 | |
123 | private: |
124 | static bool IsMemorySubsystem(const char *strTok){ |
125 | return strcmp("memory" , strTok) == 0; |
126 | } |
127 | |
128 | static bool IsCpuSubsystem(const char *strTok){ |
129 | return strcmp("cpu" , strTok) == 0; |
130 | } |
131 | |
132 | static char* FindCgroupPath(bool (*is_subsystem)(const char *)){ |
133 | char *cgroup_path = nullptr; |
134 | char *hierarchy_mount = nullptr; |
135 | char *hierarchy_root = nullptr; |
136 | char *cgroup_path_relative_to_mount = nullptr; |
137 | size_t len; |
138 | |
139 | FindHierarchyMount(is_subsystem, &hierarchy_mount, &hierarchy_root); |
140 | if (hierarchy_mount == nullptr || hierarchy_root == nullptr) |
141 | goto done; |
142 | |
143 | cgroup_path_relative_to_mount = FindCGroupPathForSubsystem(is_subsystem); |
144 | if (cgroup_path_relative_to_mount == nullptr) |
145 | goto done; |
146 | |
147 | len = strlen(hierarchy_mount); |
148 | len += strlen(cgroup_path_relative_to_mount); |
149 | cgroup_path = (char*)PAL_malloc(len+1); |
150 | if (cgroup_path == nullptr) |
151 | goto done; |
152 | |
153 | strcpy_s(cgroup_path, len+1, hierarchy_mount); |
154 | // For a host cgroup, we need to append the relative path. |
155 | // In a docker container, the root and relative path are the same and we don't need to append. |
156 | if (strcmp(hierarchy_root, cgroup_path_relative_to_mount) != 0) |
157 | strcat_s(cgroup_path, len+1, cgroup_path_relative_to_mount); |
158 | |
159 | done: |
160 | PAL_free(hierarchy_mount); |
161 | PAL_free(hierarchy_root); |
162 | PAL_free(cgroup_path_relative_to_mount); |
163 | return cgroup_path; |
164 | } |
165 | |
166 | static void FindHierarchyMount(bool (*is_subsystem)(const char *), char** pmountpath, char** pmountroot) |
167 | { |
168 | char *line = nullptr; |
169 | size_t lineLen = 0, maxLineLen = 0; |
170 | char *filesystemType = nullptr; |
171 | char *options = nullptr; |
172 | char *mountpath = nullptr; |
173 | char *mountroot = nullptr; |
174 | |
175 | FILE *mountinfofile = fopen(PROC_MOUNTINFO_FILENAME, "r" ); |
176 | if (mountinfofile == nullptr) |
177 | goto done; |
178 | |
179 | while (getline(&line, &lineLen, mountinfofile) != -1) |
180 | { |
181 | if (filesystemType == nullptr || lineLen > maxLineLen) |
182 | { |
183 | PAL_free(filesystemType); |
184 | PAL_free(options); |
185 | filesystemType = (char*)PAL_malloc(lineLen+1); |
186 | if (filesystemType == nullptr) |
187 | goto done; |
188 | options = (char*)PAL_malloc(lineLen+1); |
189 | if (options == nullptr) |
190 | goto done; |
191 | maxLineLen = lineLen; |
192 | } |
193 | char* separatorChar = strstr(line, " - " );; |
194 | |
195 | // See man page of proc to get format for /proc/self/mountinfo file |
196 | int sscanfRet = sscanf_s(separatorChar, |
197 | " - %s %*s %s" , |
198 | filesystemType, lineLen+1, |
199 | options, lineLen+1); |
200 | if (sscanfRet != 2) |
201 | { |
202 | _ASSERTE(!"Failed to parse mount info file contents with sscanf_s." ); |
203 | goto done; |
204 | } |
205 | |
206 | if (strncmp(filesystemType, "cgroup" , 6) == 0) |
207 | { |
208 | char* context = nullptr; |
209 | char* strTok = strtok_s(options, "," , &context); |
210 | while (strTok != nullptr) |
211 | { |
212 | if (is_subsystem(strTok)) |
213 | { |
214 | mountpath = (char*)PAL_malloc(lineLen+1); |
215 | if (mountpath == nullptr) |
216 | goto done; |
217 | mountroot = (char*)PAL_malloc(lineLen+1); |
218 | if (mountroot == nullptr) |
219 | goto done; |
220 | |
221 | sscanfRet = sscanf_s(line, |
222 | "%*s %*s %*s %s %s " , |
223 | mountroot, lineLen+1, |
224 | mountpath, lineLen+1); |
225 | if (sscanfRet != 2) |
226 | _ASSERTE(!"Failed to parse mount info file contents with sscanf_s." ); |
227 | |
228 | // assign the output arguments and clear the locals so we don't free them. |
229 | *pmountpath = mountpath; |
230 | *pmountroot = mountroot; |
231 | mountpath = mountroot = nullptr; |
232 | goto done; |
233 | } |
234 | strTok = strtok_s(nullptr, "," , &context); |
235 | } |
236 | } |
237 | } |
238 | done: |
239 | PAL_free(mountpath); |
240 | PAL_free(mountroot); |
241 | PAL_free(filesystemType); |
242 | PAL_free(options); |
243 | free(line); |
244 | if (mountinfofile) |
245 | fclose(mountinfofile); |
246 | } |
247 | |
248 | static char* FindCGroupPathForSubsystem(bool (*is_subsystem)(const char *)) |
249 | { |
250 | char *line = nullptr; |
251 | size_t lineLen = 0; |
252 | size_t maxLineLen = 0; |
253 | char *subsystem_list = nullptr; |
254 | char *cgroup_path = nullptr; |
255 | bool result = false; |
256 | |
257 | FILE *cgroupfile = fopen(PROC_CGROUP_FILENAME, "r" ); |
258 | if (cgroupfile == nullptr) |
259 | goto done; |
260 | |
261 | while (!result && getline(&line, &lineLen, cgroupfile) != -1) |
262 | { |
263 | if (subsystem_list == nullptr || lineLen > maxLineLen) |
264 | { |
265 | PAL_free(subsystem_list); |
266 | PAL_free(cgroup_path); |
267 | subsystem_list = (char*)PAL_malloc(lineLen+1); |
268 | if (subsystem_list == nullptr) |
269 | goto done; |
270 | cgroup_path = (char*)PAL_malloc(lineLen+1); |
271 | if (cgroup_path == nullptr) |
272 | goto done; |
273 | maxLineLen = lineLen; |
274 | } |
275 | |
276 | // See man page of proc to get format for /proc/self/cgroup file |
277 | int sscanfRet = sscanf_s(line, |
278 | "%*[^:]:%[^:]:%s" , |
279 | subsystem_list, lineLen+1, |
280 | cgroup_path, lineLen+1); |
281 | if (sscanfRet != 2) |
282 | { |
283 | _ASSERTE(!"Failed to parse cgroup info file contents with sscanf_s." ); |
284 | goto done; |
285 | } |
286 | |
287 | char* context = nullptr; |
288 | char* strTok = strtok_s(subsystem_list, "," , &context); |
289 | while (strTok != nullptr) |
290 | { |
291 | if (is_subsystem(strTok)) |
292 | { |
293 | result = true; |
294 | break; |
295 | } |
296 | strTok = strtok_s(nullptr, "," , &context); |
297 | } |
298 | } |
299 | done: |
300 | PAL_free(subsystem_list); |
301 | if (!result) |
302 | { |
303 | PAL_free(cgroup_path); |
304 | cgroup_path = nullptr; |
305 | } |
306 | free(line); |
307 | if (cgroupfile) |
308 | fclose(cgroupfile); |
309 | return cgroup_path; |
310 | } |
311 | |
312 | static bool ReadMemoryValueFromFile(const char* filename, size_t* val) |
313 | { |
314 | return ::ReadMemoryValueFromFile(filename, val); |
315 | } |
316 | |
317 | static long long ReadCpuCGroupValue(const char* subsystemFilename){ |
318 | char *filename = nullptr; |
319 | bool result = false; |
320 | long long val; |
321 | size_t len; |
322 | |
323 | if (s_cpu_cgroup_path == nullptr) |
324 | return -1; |
325 | |
326 | len = strlen(s_cpu_cgroup_path); |
327 | len += strlen(subsystemFilename); |
328 | filename = (char*)PAL_malloc(len + 1); |
329 | if (filename == nullptr) |
330 | return -1; |
331 | |
332 | strcpy_s(filename, len+1, s_cpu_cgroup_path); |
333 | strcat_s(filename, len+1, subsystemFilename); |
334 | result = ReadLongLongValueFromFile(filename, &val); |
335 | PAL_free(filename); |
336 | if (!result) |
337 | return -1; |
338 | |
339 | return val; |
340 | } |
341 | |
342 | static bool ReadLongLongValueFromFile(const char* filename, long long* val) |
343 | { |
344 | bool result = false; |
345 | char *line = nullptr; |
346 | size_t lineLen = 0; |
347 | |
348 | if (val == nullptr) |
349 | return false;; |
350 | |
351 | FILE* file = fopen(filename, "r" ); |
352 | if (file == nullptr) |
353 | goto done; |
354 | |
355 | if (getline(&line, &lineLen, file) == -1) |
356 | goto done; |
357 | |
358 | errno = 0; |
359 | *val = atoll(line); |
360 | if (errno != 0) |
361 | goto done; |
362 | |
363 | result = true; |
364 | done: |
365 | if (file) |
366 | fclose(file); |
367 | free(line); |
368 | return result; |
369 | } |
370 | }; |
371 | |
372 | char *CGroup::s_memory_cgroup_path = nullptr; |
373 | char *CGroup::s_cpu_cgroup_path = nullptr; |
374 | |
375 | void InitializeCGroup() |
376 | { |
377 | CGroup::Initialize(); |
378 | } |
379 | |
380 | void CleanupCGroup() |
381 | { |
382 | CGroup::Cleanup(); |
383 | } |
384 | |
385 | |
386 | size_t |
387 | PALAPI |
388 | PAL_GetRestrictedPhysicalMemoryLimit() |
389 | { |
390 | size_t physical_memory_limit; |
391 | |
392 | if (!CGroup::GetPhysicalMemoryLimit(&physical_memory_limit)) |
393 | physical_memory_limit = SIZE_T_MAX; |
394 | |
395 | struct rlimit curr_rlimit; |
396 | size_t rlimit_soft_limit = (size_t)RLIM_INFINITY; |
397 | if (getrlimit(RLIMIT_AS, &curr_rlimit) == 0) |
398 | { |
399 | rlimit_soft_limit = curr_rlimit.rlim_cur; |
400 | } |
401 | physical_memory_limit = std::min(physical_memory_limit, rlimit_soft_limit); |
402 | |
403 | // Ensure that limit is not greater than real memory size |
404 | long pages = sysconf(_SC_PHYS_PAGES); |
405 | if (pages != -1) |
406 | { |
407 | long pageSize = sysconf(_SC_PAGE_SIZE); |
408 | if (pageSize != -1) |
409 | { |
410 | physical_memory_limit = std::min(physical_memory_limit, |
411 | (size_t)(pages * pageSize)); |
412 | } |
413 | } |
414 | |
415 | if(physical_memory_limit == SIZE_T_MAX) |
416 | physical_memory_limit = 0; |
417 | return physical_memory_limit; |
418 | } |
419 | |
420 | BOOL |
421 | PALAPI |
422 | PAL_GetPhysicalMemoryUsed(size_t* val) |
423 | { |
424 | BOOL result = false; |
425 | size_t linelen; |
426 | char* line = nullptr; |
427 | |
428 | if (val == nullptr) |
429 | return FALSE; |
430 | |
431 | // Linux uses cgroup usage to trigger oom kills. |
432 | if (CGroup::GetPhysicalMemoryUsage(val)) |
433 | return TRUE; |
434 | |
435 | // process resident set size. |
436 | FILE* file = fopen(PROC_STATM_FILENAME, "r" ); |
437 | if (file != nullptr && getline(&line, &linelen, file) != -1) |
438 | { |
439 | char* context = nullptr; |
440 | char* strTok = strtok_s(line, " " , &context); |
441 | strTok = strtok_s(nullptr, " " , &context); |
442 | |
443 | errno = 0; |
444 | *val = strtoull(strTok, nullptr, 0); |
445 | if(errno == 0) |
446 | { |
447 | *val = *val * GetVirtualPageSize(); |
448 | result = true; |
449 | } |
450 | } |
451 | |
452 | if (file) |
453 | fclose(file); |
454 | free(line); |
455 | return result; |
456 | } |
457 | |
458 | BOOL |
459 | PALAPI |
460 | PAL_GetCpuLimit(UINT* val) |
461 | { |
462 | if (val == nullptr) |
463 | return FALSE; |
464 | |
465 | return CGroup::GetCpuLimit(val); |
466 | } |
467 | |