Skip to content

Commit 0801048

Browse files
authored
linux: follow up on cpulist parsing (#2973)
`_get_eligible_cpus()` was only parsing a single `N-M` range, so a value like `0-3,8` would fall back to all CPUs instead of returning the actual eligible CPU list. This adds `_parse_cpulist()` returning the expanded CPU IDs and uses it in `_get_eligible_cpus()`. For example: `0-3,8` -> `[0, 1, 2, 3, 8]` Also added parser coverage and a regression test for the non-contiguous `Cpus_allowed_list` case. Tested with: * `tests/test_linux.py`: 131 passed, 4 skipped * Ruff * Black * `git diff --check`
1 parent 10325a9 commit 0801048

2 files changed

Lines changed: 45 additions & 10 deletions

File tree

psutil/_pslinux.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,29 @@ def per_cpu_times():
493493
return cpus
494494

495495

496+
def _parse_cpulist(cpulist):
497+
"""Parse Linux CPU list string (e.g "0-3,8,10-11")"""
498+
cpulist = cpulist.strip()
499+
if not cpulist:
500+
return []
501+
cpus = []
502+
for chunk in cpulist.split(','):
503+
chunk = chunk.strip()
504+
if not chunk:
505+
continue
506+
if '-' in chunk:
507+
start, _, end = chunk.partition('-')
508+
start = int(start)
509+
end = int(end)
510+
if start > end:
511+
msg = f"invalid CPU range {chunk!r}"
512+
raise ValueError(msg)
513+
cpus.extend(range(start, end + 1))
514+
else:
515+
cpus.append(int(chunk))
516+
return cpus
517+
518+
496519
def cpu_count_logical():
497520
"""Return the number of logical CPUs in the system."""
498521
try:
@@ -2117,14 +2140,21 @@ def cpu_affinity_get(self):
21172140
return _psutil.proc_cpu_affinity_get(self.pid)
21182141

21192142
def _get_eligible_cpus(
2120-
self, _re=re.compile(br"Cpus_allowed_list:\t(\d+)-(\d+)")
2143+
self,
2144+
_re=re.compile(
2145+
br"^Cpus_allowed_list:[ \t]*([^\r\n]*)", re.MULTILINE
2146+
),
21212147
):
21222148
# See: https://github.com/giampaolo/psutil/issues/956
21232149
data = self._read_status_file()
2124-
if match := _re.findall(data):
2125-
return list(range(int(match[0][0]), int(match[0][1]) + 1))
2126-
else:
2127-
return list(range(len(per_cpu_times())))
2150+
if match := _re.search(data):
2151+
try:
2152+
return _parse_cpulist(decode(match.group(1)))
2153+
except ValueError as err:
2154+
debug(
2155+
f"can't parse Cpus_allowed_list ({err}); falling back"
2156+
)
2157+
return list(range(len(per_cpu_times())))
21282158

21292159
@wrap_exceptions
21302160
def cpu_affinity_set(self, cpus):

tests/test_linux.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from psutil._pslinux import CLOCK_TICKS
5353
from psutil._pslinux import RootFsDeviceFinder
5454
from psutil._pslinux import _cpu_get_cpuinfo_freq
55+
from psutil._pslinux import _parse_cpulist
5556
from psutil._pslinux import calculate_avail_vmem
5657
from psutil._pslinux import open_binary
5758

@@ -2507,6 +2508,12 @@ def test_status_file_parsing(self):
25072508
assert gids.saved == 1006
25082509
assert p._proc._get_eligible_cpus() == list(range(8))
25092510

2511+
def test_status_file_cpus_allowed_list(self):
2512+
content = b"Cpus_allowed_list:\t0-3,8\n"
2513+
with mock_open_content({f"/proc/{os.getpid()}/status": content}):
2514+
p = psutil.Process()
2515+
assert p._proc._get_eligible_cpus() == [0, 1, 2, 3, 8]
2516+
25102517
def test_net_connections_enametoolong(self):
25112518
# Simulate a case where /proc/{pid}/fd/{fd} symlink points to
25122519
# a file with full path longer than PATH_MAX, see:
@@ -2620,11 +2627,9 @@ def test_cpu_affinity(self):
26202627
def test_cpu_affinity_eligible_cpus(self):
26212628
value = self.read_status_file("Cpus_allowed_list:")
26222629
with mock.patch("psutil._pslinux.per_cpu_times") as m:
2623-
self.proc._proc._get_eligible_cpus()
2624-
if '-' in str(value):
2625-
assert not m.called
2626-
else:
2627-
assert m.called
2630+
cpus = self.proc._proc._get_eligible_cpus()
2631+
assert cpus == _parse_cpulist(str(value))
2632+
assert not m.called
26282633

26292634

26302635
# =====================================================================

0 commit comments

Comments
 (0)