Đây là một bài viết về một khái niệm đơn giản. Trong .NET, tôi muốn biết có bao nhiêu bộ xử lý tồn tại trên máy chủ/máy ảo. Tuy nhiên, theo những gì tôi tìm hiểu, không có API nào cho việc này trong .NET hiện đại. Nếu bạn cần thông tin này, bài viết này hiển thị cách tiếp cận duy nhất tôi có thể nghĩ ra, bao gồm P/Invoke trên một số nền tảng và phân tích cú pháp các tệp trên Linux!
Nếu có cách tốt hơn, hãy cho tôi biết, tôi khá ghét những gì tôi phải làm ở đây 😅
Mục lục
Tại sao không dùng Environment.ProcessorCount?
Hy vọng ai đó đang nghĩ “Tại sao bạn không dùng Environment.ProcessorCount?”. Dù sao thì nó đã có sẵn từ .NET Framework 2.0! Thật không may, giá trị này thực sự có nghĩa gì phụ thuộc vào phiên bản .NET bạn đang sử dụng…
- .NET Framework—Trả về số lượng bộ xử lý logic trên máy chủ (chính xác là những gì tôi muốn 🎉)
- .NET Core < 6—Trả về số lượng bộ xử lý logic trên máy chủ, nhưng nhận biết container (đến một mức độ nào đó, mặc dù nó có bug)
- .NET Core 6+—Trả về số lượng bộ xử lý logic trên máy chủ trừ khi bạn chạy với sự gắn kết tiến trình, hoặc bạn đang chạy trong container. Về cơ bản nó trả về số lượng bộ xử lý dành cho tiến trình.
Trớ trêu thay, .NET Framework thực sự làm đúng những gì tôi cần trong khi .NET hiện đại thì không 😅 Điều này thường có thể hiểu được, vì thông thường việc biết bao nhiêu tiến trình có sẵn cho một tiến trình hữu ích hơn, thay vì biết máy chủ có bao nhiêu, nhưng trong trường hợp này, đó không phải những gì tôi muốn.
Vậy chúng ta có những lựa chọn nào?
Microsoft.Extensions.Diagnostics.ResourceMonitoring có thể là câu trả lời?
Luật Betteridge của tiêu đề áp dụng ở đây, câu trả lời là “Không” 😅
Nếu bạn chưa biết, gói NuGet Microsoft.Extensions.Diagnostics.ResourceMonitoring cung cấp một bộ API để theo dõi việc sử dụng tài nguyên (CPU, bộ nhớ, mạng) của các ứng dụng .NET của bạn.
Nó cung cấp hai bộ API
- Các dịch vụ đã lỗi thời như
IResourceMonitormà bạn có thể sử dụng để truy xuất thủ công các giá trị quan tâm - API System.Diagnostics.Metrics sử dụng
Meters để tạo ra các chỉ số tương thích với OpenTelemetry
Bạn có thể sử dụng các chỉ số này để phát ra nhiều chỉ số tài nguyên:
container.cpu.limit.utilizationcontainer.cpu.request.utilizationcontainer.cpu.timecontainer.memory.limit.utilizationcontainer.memory.usageprocess.cpu.utilizationdotnet.process.memory.virtual.utilizationsystem.network.connections
Tuy nhiên, bạn sẽ nhận thấy rằng không có chỉ số nào trong số đó là số lượng bộ xử lý máy chủ. Vì vậy chúng ta hết hy vọng.
Gọi API gốc để lấy chi tiết
Tôi sẽ đi thẳng vào vấn đề: cách duy nhất tôi tìm được để truy xuất các giá trị tôi muốn là gọi API gốc:
- Trên Windows, chúng ta cần P/Invoke
GetActiveProcessorCount() - Trên macOS, chúng ta cần P/Invoke
sysctlbyname("hw.logicalcpu") - Trên Linux, chúng ta cần đọc và phân tích cú pháp
/sys/devices/system/cpu/online
Điều này bừa bộn hơn nhiều so với tôi hy vọng, vì vậy nếu ai đó có cách tiếp cận tốt hơn, tôi rất sẵn lòng lắng nghe! Tuy nhiên, các phần sau đây mô tả cách đọc từng giá trị này trên ba nền tảng.
Lấy tổng số CPU trên Windows
Tôi sẽ bắt đầu với Windows, vì đây là một trong những cách đơn giản nhất. Chúng ta chỉ cần gọi vào kernel và gọi GetActiveProcessorCount với cờ “Tất cả nhóm bộ xử lý”, để chúng ta lấy tổng số bộ xử lý trên hệ thống:
internal static class WindowsProcessorCount
{
private const ushort AllProcessorGroups = 0xFFFF;
internal static int? GetTotalProcessorCount(ILogger log)
{
var result = GetActiveProcessorCount(AllProcessorGroups);
if (result > 0)
{
return result;
}
var error = Marshal.GetLastPInvokeError();
log.LogWarning(
"GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
property: error);
return null;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetActiveProcessorCount(ushort groupNumber);
}
Điều này rất đơn giản – phương thức GetTotalProcessorCount chỉ cần P/Invoke và trả về số lượng bộ xử lý. Nếu giá trị trả về là 0 thì chúng ta đã gặp lỗi, vì vậy chúng ta ghi nhật ký và trả về null.
Lưu ý rằng tôi đã sử dụng
[DllImport]trong tất cả các ví dụ này, vì tôi cần hỗ trợ .NET 6, nhưng nếu bạn có thể, bạn có thể sẽ muốn sử dụng[LibraryImport]thay thế.
Xong phần Windows, sang hệ điều hành tiếp theo!
Lấy tổng số CPU trên macOS
Code trên macOS cũng tương tự là một P/Invoke duy nhất, tuy nhiên nó sử dụng hàm thư viện tổng quát sysctlbyname đòi hỏi nhiều thao tác với đối số hơn Windows. Cuối cùng, nó có cùng mô hình cơ bản với code Windows.
internal static class MacOsProcessorCount
{
private const string LogicalCpuName = "hw.logicalcpu";
internal static int? GetTotalProcessorCount(ILogger log)
{
var size = new IntPtr(sizeof(int));
var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);
if (result == 0 && value > 0)
{
return value;
}
var error = Marshal.GetLastPInvokeError();
log.LogWarning(
"sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
property: error);
return null;
}
[DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
private static extern int SysCtlByName(
string name,
out int oldp,
ref IntPtr oldlenp,
IntPtr newp,
IntPtr newlen);
}
Vì vậy về cơ bản chúng ta có cùng mô hình ở đây: thực hiện P/Invoke, kiểm tra kết quả và trả về giá trị. Không quá tệ (mặc dù bạn obviously phải đảm bảo lấy đúng API P/Invoke, may mắn thay đây là điều mà LLM rất giỏi trong thời đại ngày nay).
Nền tảng cuối cùng chúng ta có là Linux, nơi mọi thứ trở nên hơi khác.
Lấy tổng số CPU trên Linux
Trên Linux, thay vì thực hiện P/Invoke vào một thư viện, chúng ta đọc từ tệp /sys/devices/system/cpu/online, phân tích cú pháp danh sách CPU và trả về kết quả.
Lưu ý rằng bạn có thể thực hiện P/Invoke vào thư viện C và gọi
sysconf(_SC_NPROCESSORS_ONLN), nhưng có các trường hợp đặc biệt khi gọisysconfmà tôi muốn tránh, chẳng hạn như thực tế là hằng số khác nhau giữa glibc và musl等等. Bằng cách đọc trực tiếp tệp, chúng ta tránh những vấn đề đó.
internal static class LinuxProcessorCount
{
private const string OnlineCpusPath = "/sys/devices/system/cpu/online";
internal static int? GetTotalProcessorCount(ILogger log)
{
try
{
var contents = File.ReadAllText(OnlineCpusPath);
var result = TryParseOnlineCpuRanges(contents.AsSpan());
if (result is null)
{
Log.LogWarning(ex, "Parsing cpu-list failed: contents was not a valid cpu-list '{FileContents}'", contents);
}
}
catch (Exception ex)
{
Log.LogWarning(ex, $"Error reading '${OnlineCpusPath}' to determine total machine processor count");
return null;
}
}
// Parses the Linux cpu-list-format (see https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)
// This is a comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,4,8-11".
internal static int? TryParseOnlineCpuRanges(ReadOnlySpan<char> contents)
{
var trimmed = contents.Trim();
if (trimmed.IsEmpty)
{
return null;
}
var count = 0;
var remaining = trimmed;
while (!remaining.IsEmpty)
{
// Find the next token
var commaIndex = remaining.IndexOf(',');
var token = commaIndex < 0 ? remaining : remaining[..commaIndex];
if (!TryParseToken(token, out var tokenCount))
{
// Should never happen, means the file contained invalid data
return null;
}
// Increase the CPU count
count += tokenCount;
if (commaIndex < 0)
{
// All done
break;
}
// Cut off the values we just read
remaining = remaining[(commaIndex + 1)..];
if (remaining.IsEmpty)
{
// trailing comma with no following token
return null;
}
}
// If we didn't read any values, something weird happened
return count > 0 ? count : null;
// Parse either a single value like "4", or a range, like "3-7"
static bool TryParseToken(ReadOnlySpan<char> token, out int tokenCount)
{
tokenCount = 0;
var dashIndex = token.IndexOf('-');
if (dashIndex < 0)
{
// A single value
if (!int.TryParse(token, out var single) || single < 0)
{
return false;
}
tokenCount = 1;
return true;
}
// Parse each value in the range
var startSpan = token[..dashIndex];
var endSpan = token[(dashIndex + 1)..];
if (!int.TryParse(startSpan, out var start) || start < 0 ||
!int.TryParse(endSpan, out var end) || end < start)
{
return false;
}
// Count the number covered by the range, e.g 0-3 = 4 CPUs
tokenCount = end - start + 1;
return true;
}
}
}
Như tôi đã nói trước đó, điều này hơi khó chịu và phức tạp, nhưng nó không phức tạp, nó chỉ đọc một tệp và phân tích cú pháp nội dung 🙂
Gộp tất cả lại với nhau
Vì vậy bây giờ chúng ta có một phương thức để đọc tổng số CPU trên mỗi nền tảng, chúng ta có thể gộp tất cả lại thành một phương thức tiện lợi, gọi API đúng dựa trên nền tảng:
internal static class TotalProcessorCount
{
internal static int? GetTotalProcessorCount(ILogger log)
{
if (OperatingSystem.IsWindows())
{
return WindowsProcessorCount.GetTotalProcessorCount(log);
}
if (OperatingSystem.IsLinux())
{
return LinuxProcessorCount.GetTotalProcessorCount(log);
}
if (OperatingSystem.IsMacOS())
{
return MacOsProcessorCount.GetTotalProcessorCount(log);
}
return null;
}
}
Cho bài viết này, tôi đã tạo helper dưới dạng một kiểu static đơn giản, nhưng bạn có thể sẽ muốn cached giá trị trả về từ GetTotalProcessorCount() vì nó sẽ không thay đổi trong suốt tuổi thọ của tiến trình (trừ khi chúng ta đã làm sai điều gì đó!). Tôi sẽ để lại điều đó cho người đọc, nhưng để đầy đủ, đây là kiểu đầy đủ, với các kiểu helper lồng bên trong để đóng gói chúng:
internal static class TotalProcessorCount
{
internal static int? GetTotalProcessorCount(ILogger log)
{
if (OperatingSystem.IsWindows())
{
return WindowsProcessorCount.GetTotalProcessorCount(log);
}
if (OperatingSystem.IsLinux())
{
return LinuxProcessorCount.GetTotalProcessorCount(log);
}
if (OperatingSystem.IsMacOS())
{
return MacOsProcessorCount.GetTotalProcessorCount(log);
}
return null;
}
private static class WindowsProcessorCount
{
private const ushort AllProcessorGroups = 0xFFFF;
internal static int? GetTotalProcessorCount(ILogger log)
{
var result = GetActiveProcessorCount(AllProcessorGroups);
if (result > 0)
{
return result;
}
var error = Marshal.GetLastPInvokeError();
log.LogWarning(
"GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
property: error);
return null;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetActiveProcessorCount(ushort groupNumber);
}
private static class MacOsProcessorCount
{
private const string LogicalCpuName = "hw.logicalcpu";
internal static int? GetTotalProcessorCount(ILogger log)
{
var size = new IntPtr(sizeof(int));
var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);
if (result == 0 && value > 0)
{
return value;
}
var error = Marshal.GetLastPInvokeError();
log.LogWarning(
"sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
property: error);
return null;
}
[DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
private static extern int SysCtlByName(
string name,
out int oldp,
ref IntPtr oldlenp,
IntPtr newp,
IntPtr newlen);
}
internal static class LinuxProcessorCount
{
private const string OnlineCpusPath = "/sys/devices/system/cpu/online";
internal static int? GetTotalProcessorCount(ILogger log)
{
try
{
var contents = File.ReadAllText(OnlineCpusPath);
var result = TryParseOnlineCpuRanges(contents.AsSpan());
if (result is null)
{
Log.LogWarning(ex, "Parsing cpu-list failed: contents was not a valid cpu-list '{FileContents}'", contents);
}
}
catch (Exception ex)
{
Log.LogWarning(ex, $"Error reading '${OnlineCpusPath}' to determine total machine processor count");
return null;
}
}
// Parses the Linux cpu-list-format (see https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)
// This is a comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,4,8-11".
private static int? TryParseOnlineCpuRanges(ReadOnlySpan<char> contents)
{
var trimmed = contents.Trim();
if (trimmed.IsEmpty)
{
return null;
}
var count = 0;
var remaining = trimmed;
while (!remaining.IsEmpty)
{
// Find the next token
var commaIndex = remaining.IndexOf(',');
var token = commaIndex < 0 ? remaining : remaining[..commaIndex];
if (!TryParseToken(token, out var tokenCount))
{
// Should never happen, means the file contained invalid data
return null;
}
// Increase the CPU count
count += tokenCount;
if (commaIndex < 0)
{
// All done
break;
}
// Cut off the values we just read
remaining = remaining[(commaIndex + 1)..];
if (remaining.IsEmpty)
{
// trailing comma with no following token
return null;
}
}
// If we didn't read any values, something weird happened
return count > 0 ? count : null;
// Parse either a single value like "4", or a range, like "3-7"
static bool TryParseToken(ReadOnlySpan<char> token, out int tokenCount)
{
tokenCount = 0;
var dashIndex = token.IndexOf('-');
if (dashIndex < 0)
{
// A single value
if (!int.TryParse(token, out var single) || single < 0)
{
return false;
}
tokenCount = 1;
return true;
}
// Parse each value in the range
var startSpan = token[..dashIndex];
var endSpan = token[(dashIndex + 1)..];
if (!int.TryParse(startSpan, out var start) || start < 0 ||
!int.TryParse(endSpan, out var end) || end < start)
{
return false;
}
// Count the number covered by the range, e.g 0-3 = 4 CPUs
tokenCount = end - start + 1;
return true;
}
}
}
}
Tôi có nên dùng code này không?
Điều đó hoàn toàn phụ thuộc vào bạn 😅 Tôi chưa triển khai code này vào sản xuất, nhưng tôi đang nghiêm túc cân nhắc. Tôi nghĩ nó khá vững chắc theo những gì tôi (và 🤖) có thể nói, nhưng obvious là hãy sử dụng sự phán đoán của riêng bạn. Như tôi đã nói trước đây, nếu bạn biết cách tốt hơn để lấy những giá trị này, tôi rất quan tâm muốn nghe trong phần bình luận.
Một điều tôi sẽ gợi ý thay đổi nếu bạn đang sử dụng các ứng dụng .NET hiện đại với dependency injection等等, là lồng tất cả code này vào một wrapper singleton nhỏ cached giá trị trong suốt tuổi thō của tiến trình và cung cấp một thể hiện ILogger để sử dụng等等. Nhưng ngoài ra, hãy thử nó, đảm bảo nó hoạt động cho bạn!
Tóm tắt
Trong bài viết này, tôi đã nói về cách tìm tổng số CPU có sẵn trên một máy chủ, trái ngược với số CPU có sẵn cho một tiến trình. Environment.ProcessorCount trả về cái đầu tiên trong .NET Framework, nhưng trong .NET Core, nó trả về cái sau (và bạn thực sự có thể tin tưởng các giá trị từ khoảng .NET 6+). Tuy nhiên, trong .NET 6+, nếu bạn thực sự muốn tổng số CPU trên máy chủ, thì không có API managed nào tôi có thể tìm thấy trong BCL để đạt được điều đó.
Là kết quả, trong bài viết này, tôi chỉ cách tìm tổng số bộ xử lý trên Windows, macOS và Linux. Với Windows và macOS, chúng ta có thể sử dụng P/Invoke đơn giản để đọc giá trị. Điều này về mặt lý thuyết có sẵn trên Linux, nhưng nó khó hơn bạn có thể mong đợi, vì vậy thay vì sử dụng P/Invoke, tôi chỉ cách đọc và phân tích cú pháp tệp /sys/devices/system/cpu/online. Cuối cùng, tôi gộp cả ba cách tiếp cận vào một helper chuyển đổi dựa trên nền tảng hiện tại.



