C# 启动进程会泄漏内存,即使已终止并已处置(在 Linux 上)
C# Starting Process leaks memory even though Killed and Disposed (on Linux)
注意:根据测试(见下面的编辑),这只发生在 Linux 机器上。
我有一个 ASP.NET Core Blazor 应用程序(使用服务器端托管模型)运行正在 Raspberry Pi 上运行。该应用程序的部分功能是 dim/brighten 根据系统最后一次交互的时间进行屏幕显示。为此,我每隔 1 秒左右生成一个终端子进程到 运行 xprintidle
,解析其输出,并相应地采取行动。
我用DataDog做监控,内存泄漏一直到系统崩溃(用完所有内存需要几天时间,但最终还是会发生):
我已经指出以下方法是内存泄漏的原因 - 如果我跳过调用它并使用一些常量时间跨度,则内存不会泄漏:
我有以下代码可以这样做:
// note this code has some parts that aren't even needed - I was simply trying anything to solve this problem at this point
public async Task<TerminalResult> ExecuteAndWaitAsync(string command, bool asRoot, CancellationToken cancellationToken = default)
{
using Process prc = CreateNewProcess(command, asRoot);
// we need to redirect stdstreams to read them
prc.StartInfo.RedirectStandardOutput = true;
prc.StartInfo.RedirectStandardError = true;
// start the process
_log.LogTrace("Starting the process");
using Task waitForExitTask = WaitForExitAsync(prc, cancellationToken);
prc.Start();
// read streams
string[] streamResults = await Task.WhenAll(prc.StandardOutput.ReadToEndAsync(), prc.StandardError.ReadToEndAsync()).ConfigureAwait(false);
// wait till it fully exits, but no longer than half a second
// this prevents hanging when process has already finished, but takes long time to fully close
await Task.WhenAny(waitForExitTask, Task.Delay(500, cancellationToken)).ConfigureAwait(false);
// if process still didn't exit, force kill it
if (!prc.HasExited)
prc.Kill(true); // doing it with a try-catch approach instead of HasExited check gives no difference
return new TerminalResult(streamResults[0], streamResults[1]);
}
public Task<int> WaitForExitAsync(Process process, CancellationToken cancellationToken = default)
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
IDisposable tokenRegistration = null;
EventHandler callback = null;
tokenRegistration = cancellationToken.Register(() =>
{
Unregister();
tcs.TrySetCanceled(cancellationToken);
});
callback = (sender, args) =>
{
Unregister();
tcs.TrySetResult(process.ExitCode);
};
process.Exited += callback;
process.EnableRaisingEvents = true;
void Unregister()
{
lock (tcs)
{
if (tokenRegistration == null)
return;
process.EnableRaisingEvents = false;
process.Exited -= callback;
tokenRegistration?.Dispose();
tokenRegistration = null;
}
}
return tcs.Task;
}
private Process CreateNewProcess(string command, bool asRoot)
{
_log.LogDebug("Creating process: {Command}", command);
Process prc = new Process();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string escapedCommand = command.Replace("\"", "\\"");
// if as root, just sudo it
if (asRoot)
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"sudo {escapedCommand}\"");
// if not as root, we need to open it as current user
// this may still run as root if the process is running as root
else
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"{escapedCommand}\"");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
prc.StartInfo = new ProcessStartInfo("CMD.exe", $"/C {command}");
if (asRoot)
prc.StartInfo.Verb = "runas";
}
else
throw new PlatformNotSupportedException($"{nameof(ExecuteAndWaitAsync)} is only supported on Windows and Linux platforms.");
prc.StartInfo.UseShellExecute = false;
prc.StartInfo.CreateNoWindow = true;
if (_log.IsEnabled(LogLevel.Trace))
{
_log.LogTrace("exec: {FileName} {Args}", prc.StartInfo.FileName, prc.StartInfo.Arguments);
_log.LogTrace("exec: as root = {AsRoot}", asRoot);
}
return prc;
}
我花了很多时间(几个月的时间 - 从字面上看)尝试各种更改来解决这个问题 - WaitForExitAsync
进行了很多大修,尝试了不同的处理方式。我试图定期调用 GC.Collect() 。还尝试了 运行使用服务器和工作站 GC 模式对应用程序进行调试。
正如我之前提到的,我很确定是这段代码泄漏了——如果我不调用 ExecuteAndWaitAsync
,就没有内存泄漏。结果 class 也没有被调用者存储 - 它只是解析一个值并立即使用它:
public async Task<TimeSpan> GetSystemIdleTimeAsync(CancellationToken cancellationToken = default)
{
ThrowIfNotLinux();
const string prc = "xprintidle";
TerminalResult result = await _terminal.ExecuteAndWaitAsync(prc, false, cancellationToken).ConfigureAwait(false);
if (result.HasErrors || !int.TryParse(result.Output, out int idleMs))
throw new InvalidOperationException($"{prc} returned invalid data.");
return TimeSpan.FromMilliseconds(idleMs);
}
private static void ThrowIfNotLinux()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
throw new PlatformNotSupportedException($"{nameof(BacklightControl)} is only functional on Linux systems.");
}
我错过了什么吗?是进程 class 泄漏,还是我读取输出的方式?
EDIT:正如评论中的人所问,我创建了最小的 运行nable 代码,基本上获取了所有相关方法一个 class 并循环执行。该代码可作为要点使用:https://gist.github.com/TehGM/c953b670ad8019b2b2be6af7b14807c2
我 运行 在我的 Windows 机器和 Raspberry Pi 上都使用了它。在 Windows 上,内存似乎很稳定,但在 Raspberry Pi 上,它显然正在泄漏。我尝试了 xprintidle
和 ifconfig
以确保这不是仅 xprintidle 的问题。 .NET Core 3.0和.NET Core 3.1都试过了,效果基本一样。
可能是.NET Core 2.2和.NET Core 3.0之间的回归导致的
显然它会在版本 3.1.7
中修复
由于未释放句柄
,刚启动进程会导致 linux 内存泄漏
注意:根据测试(见下面的编辑),这只发生在 Linux 机器上。
我有一个 ASP.NET Core Blazor 应用程序(使用服务器端托管模型)运行正在 Raspberry Pi 上运行。该应用程序的部分功能是 dim/brighten 根据系统最后一次交互的时间进行屏幕显示。为此,我每隔 1 秒左右生成一个终端子进程到 运行 xprintidle
,解析其输出,并相应地采取行动。
我用DataDog做监控,内存泄漏一直到系统崩溃(用完所有内存需要几天时间,但最终还是会发生):
我已经指出以下方法是内存泄漏的原因 - 如果我跳过调用它并使用一些常量时间跨度,则内存不会泄漏: 我有以下代码可以这样做:
// note this code has some parts that aren't even needed - I was simply trying anything to solve this problem at this point
public async Task<TerminalResult> ExecuteAndWaitAsync(string command, bool asRoot, CancellationToken cancellationToken = default)
{
using Process prc = CreateNewProcess(command, asRoot);
// we need to redirect stdstreams to read them
prc.StartInfo.RedirectStandardOutput = true;
prc.StartInfo.RedirectStandardError = true;
// start the process
_log.LogTrace("Starting the process");
using Task waitForExitTask = WaitForExitAsync(prc, cancellationToken);
prc.Start();
// read streams
string[] streamResults = await Task.WhenAll(prc.StandardOutput.ReadToEndAsync(), prc.StandardError.ReadToEndAsync()).ConfigureAwait(false);
// wait till it fully exits, but no longer than half a second
// this prevents hanging when process has already finished, but takes long time to fully close
await Task.WhenAny(waitForExitTask, Task.Delay(500, cancellationToken)).ConfigureAwait(false);
// if process still didn't exit, force kill it
if (!prc.HasExited)
prc.Kill(true); // doing it with a try-catch approach instead of HasExited check gives no difference
return new TerminalResult(streamResults[0], streamResults[1]);
}
public Task<int> WaitForExitAsync(Process process, CancellationToken cancellationToken = default)
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
IDisposable tokenRegistration = null;
EventHandler callback = null;
tokenRegistration = cancellationToken.Register(() =>
{
Unregister();
tcs.TrySetCanceled(cancellationToken);
});
callback = (sender, args) =>
{
Unregister();
tcs.TrySetResult(process.ExitCode);
};
process.Exited += callback;
process.EnableRaisingEvents = true;
void Unregister()
{
lock (tcs)
{
if (tokenRegistration == null)
return;
process.EnableRaisingEvents = false;
process.Exited -= callback;
tokenRegistration?.Dispose();
tokenRegistration = null;
}
}
return tcs.Task;
}
private Process CreateNewProcess(string command, bool asRoot)
{
_log.LogDebug("Creating process: {Command}", command);
Process prc = new Process();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string escapedCommand = command.Replace("\"", "\\"");
// if as root, just sudo it
if (asRoot)
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"sudo {escapedCommand}\"");
// if not as root, we need to open it as current user
// this may still run as root if the process is running as root
else
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"{escapedCommand}\"");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
prc.StartInfo = new ProcessStartInfo("CMD.exe", $"/C {command}");
if (asRoot)
prc.StartInfo.Verb = "runas";
}
else
throw new PlatformNotSupportedException($"{nameof(ExecuteAndWaitAsync)} is only supported on Windows and Linux platforms.");
prc.StartInfo.UseShellExecute = false;
prc.StartInfo.CreateNoWindow = true;
if (_log.IsEnabled(LogLevel.Trace))
{
_log.LogTrace("exec: {FileName} {Args}", prc.StartInfo.FileName, prc.StartInfo.Arguments);
_log.LogTrace("exec: as root = {AsRoot}", asRoot);
}
return prc;
}
我花了很多时间(几个月的时间 - 从字面上看)尝试各种更改来解决这个问题 - WaitForExitAsync
进行了很多大修,尝试了不同的处理方式。我试图定期调用 GC.Collect() 。还尝试了 运行使用服务器和工作站 GC 模式对应用程序进行调试。
正如我之前提到的,我很确定是这段代码泄漏了——如果我不调用 ExecuteAndWaitAsync
,就没有内存泄漏。结果 class 也没有被调用者存储 - 它只是解析一个值并立即使用它:
public async Task<TimeSpan> GetSystemIdleTimeAsync(CancellationToken cancellationToken = default)
{
ThrowIfNotLinux();
const string prc = "xprintidle";
TerminalResult result = await _terminal.ExecuteAndWaitAsync(prc, false, cancellationToken).ConfigureAwait(false);
if (result.HasErrors || !int.TryParse(result.Output, out int idleMs))
throw new InvalidOperationException($"{prc} returned invalid data.");
return TimeSpan.FromMilliseconds(idleMs);
}
private static void ThrowIfNotLinux()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
throw new PlatformNotSupportedException($"{nameof(BacklightControl)} is only functional on Linux systems.");
}
我错过了什么吗?是进程 class 泄漏,还是我读取输出的方式?
EDIT:正如评论中的人所问,我创建了最小的 运行nable 代码,基本上获取了所有相关方法一个 class 并循环执行。该代码可作为要点使用:https://gist.github.com/TehGM/c953b670ad8019b2b2be6af7b14807c2
我 运行 在我的 Windows 机器和 Raspberry Pi 上都使用了它。在 Windows 上,内存似乎很稳定,但在 Raspberry Pi 上,它显然正在泄漏。我尝试了 xprintidle
和 ifconfig
以确保这不是仅 xprintidle 的问题。 .NET Core 3.0和.NET Core 3.1都试过了,效果基本一样。
可能是.NET Core 2.2和.NET Core 3.0之间的回归导致的 显然它会在版本 3.1.7
中修复由于未释放句柄
,刚启动进程会导致 linux 内存泄漏