RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 907230
Accepted
iluxa1810
iluxa1810
Asked:2020-11-16 16:36:30 +0000 UTC2020-11-16 16:36:30 +0000 UTC 2020-11-16 16:36:30 +0000 UTC

为什么子线程中的堆栈溢出会杀死整个进程?

  • 772

回答这个问题时,我很惊讶子线程中的堆栈溢出会杀死整个进程。

其实,问题是:

为什么会这样?毕竟,每个线程都有自己的堆栈,据我所知,Richter 甚至写过它。

甚至StackTrace().FrameCount在主线程和子线程中显示不同数量的帧。

根据上一个问题中的评论,在 C++ 中,这是一个已处理的异常,但据.NET我所知,它可能不是因为堆栈短缺而导致需要做某事的 CLR 损坏。

CLR,理论上,一个用于整个应用程序,它绝对不会在子线程中旋转。

事实证明,当运行其他人的代码时,即使在单独的线程中,即使在单独的域中,我们也无法访问其源代码,我们仍然会失败,而在 .NET 中,这种类型无法防止这种情况发生例外?

UPD

有一些 CER“受限制的执行区域”,您可以在其中指定方法可以被进程信任。这对 CLR 是否有任何影响,以便它准备好并且不会死亡?

c#
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    default locale
    2020-11-16T17:49:51Z2020-11-16T17:49:51Z

    在我看来,CLR 工作的两个特征在这里重叠。

    子线程中未处理的异常会杀死进程

    不仅是堆栈溢出,而且通常子线程中任何未处理的异常都会杀死整个进程:

    static void Main()
    {
        var thread = new Thread(Recursive);
        thread.Start();
        while (true)
        {
            Console.WriteLine("I will live forever!");
            Thread.Sleep(1000);
        }
    }
    
    static void Recursive()
    {
        throw new Exception("RIP Unnamed Process (2018-2018");
    }
    

    这在 MSDN 中有说明:

    从 .NET Framework 2.0 版开始,公共语言运行时允许线程中大多数未处理的异常自然地进行。在大多数情况下,这意味着未处理的异常会导致应用程序终止。

    Eric Lippert 给出了这种行为的 [有争议的] 动机:

    我们无法轻易区分哪些是缺少处理程序的错误/外生异常,哪些是由于实现中的某些东西被破坏而导致程序崩溃的错误。最安全的做法是假设每个未处理的异常要么是致命异常,要么是未处理的愚蠢异常。在这两种情况下,正确的做法是立即取消该过程。

    这种理念是在 CLR 中实现未处理异常的基础。早在 CLR v1.0 的日子里,政策是“主”线程上的一个未处理的异常积极地取消了进程,但是“工作”线程上的一个未处理的异常只是杀死了线程并让主线程运行。(并且终结器线程上的异常被忽略并且终结器继续运行。)它导致的场景是服务器分配了一个有问题的子系统来在一堆工作线程上做一些工作;所有的工作线程都默默地关闭了,用户被困在一个服务器上,它耐心地等待着永远不会出现的结果,因为所有产生结果的线程都消失了。用户很难诊断出这样的问题;一个在一个难题上拼命工作的服务器和一个因为所有工作人员都死了而什么都不做的服务器从外面看几乎是一样的。因此,该策略在 CLR v2.0 中进行了更改,因此工作线程上未处理的异常也会默认关闭该进程。你想对你的失败大声喧哗,而不是沉默。

    这意味着需要严格处理线程中的异常,如下所示:

    static void SafeRecursive()
    {
        try
        {
            Recursive();
        }
        catch (Exception e)
        {
            //должная обработка, запись в логи и уведомление администраторам
            //ха-ха, так никто не делает, просто глотаем и забываем о потоке
        }
    }
    

    无法捕获 StackOverflowException

    但是StackOverflowException的特点是它无法被捕获,无论是在子线程中还是在其他任何地方:

    static void Main()
    {
        try
        {
            Recursive();
        }
        catch (Exception)
        {
            //не получится
            Console.WriteLine("Catch!");
        }
    }
    

    异常文档是怎么说的:

    从 .NET Framework 2.0 开始,您无法使用 try/catch 块捕获 StackOverflowException 对象,并且默认情况下会终止相应的进程。因此,您应该编写代码来检测和防止堆栈溢出。

    该文档清楚地表明,在进程的任何地方,任何地方都不应该有堆栈溢出。

    在某些退化的情况下,仍然可以处理 StackOverflowException:

    • 如果你自己加载CLR,你也许可以恢复它;
    • 如果代码抛出 StackOverflowException。

    在 .Net 1.0 中,堆栈溢出可能会被捕获,从 2.0 版本开始,开发人员采取了强硬立场:“堆栈溢出是程序员及其代码的问题,而不是 CLR。​​” 我找不到任何直接迹象表明为什么需要进行更改。我认为 SOE 之后的恢复没有问题,并且开发人员决定不在此任务上花费资源。

    总的来说,CLR 的这两个特性都非常符合清教徒式的哲学立场“死程序不会说谎”,Eric Lippert 也涵盖了这一点:

    我属于哲学派,认为软件设备的突然、灾难性故障当然是不幸的,但在许多情况下,最好让软件引起对问题的关注,以便可以修复它,而不是试图在糟糕的状态下得过且过,可能会在此过程中引入安全漏洞或破坏用户数据。

    更新:受约束的执行区域

    评论中的问题:

    和“限制执行区域”CER - 它是什么?在这里你可以挂一个属性,这个属性有可能会损坏进程。它有什么影响吗?

    影响。CER允许您保护自己免受代码执行期间可能发生的一些错误(类加载错误、内存不足)并正确释放资源。

    但是,CER 无法处理堆栈溢出。Richter 写道(CLR via C#, Constrained Execution Regions):

    注意即使所有方法都准备好了,方法调用仍然可能导致 StackOverflowException。当没有托管 CLR 时,StackOverflowException 会导致进程通过 CLR 内部调用 Environment.FailFast 立即终止。托管时,PreparedConstrainedRegions 方法检查堆栈以查看是否还有大约 48 KB 的堆栈空间剩余。如果堆栈空间有限,则在进入 try 块之前会发生 StackOverflowException。

    警告:即使所有方法都已显式准备好,调用方法仍可能导致 StackOverflowException。如果 CLR 没有从外部加载,则 StackOverflowException 会通过调用 Environment.FailFast 立即终止进程。如果 CLR 是从外部加载的,那么 PreparedConstrainedRegions 方法会检查堆栈上是否还有大约 48KB 的可用空间。如果堆栈空间有限,则在进入 try 块之前抛出 StackOverflowException。

    那。使用单个 CER 捕获堆栈溢出将不起作用。文档中还特别提到了 StackOverflowException :

    当从 try 块生成 StackOverflowException 时,使用 PrepareConstrainedRegions 方法标记的 CER 无法正常工作。有关详细信息,请参阅 ExecuteCodeWithGuaranteedCleanup 方法。

    我无法获得ExecuteCodeWithGuaranteedCleanup方法来处理堆栈溢出。根据有关此方法的问题中的讨论,它也仅在您自己托管 CLR 时才有效。

    链接

    • 在单独的线程上捕获未处理的异常- 关于单独线程中的异常处理的问题
    • 在不同线程中抛出的捕获异常是另一个。
    • 如何防止和/或处理 StackOverflowException?——防止国有企业的问题,各种拐杖都在考虑之中。
    • 什么时候可以捕获 StackOverflowException?- 一篇关于堕落案件的文章,只强调不能抓住国有企业。
    • Asynchrony in C# 5, Part 8: More Exceptions - Eric Lippert 的关于异步函数中的异常的文章,顺便讨论了 .Net 1.0 中子线程中异常处理的历史,以及总体而言 C# 作者的态度到异常处理。
    • 6
  2. MSDN.WhiteKnight
    2020-11-18T15:45:22Z2020-11-18T15:45:22Z

    根据上一个问题的评论,在 C++ 中,这是一个已处理的异常。

    事实上,一切都有些不同。当然,标准 C++ 工具无法处理堆栈溢出。但是,在 Windows 上,它可以使用 SEH 机制来处理。而且,无论 Eric Lippert 说什么,从堆栈溢出中恢复是一个完全受支持的场景,否则为什么会存在_resetstkoflw和SetThreadStackGuarantee函数?

    但不是在.NET中,因为据我所知,需要做某事的CLR可能会由于缺少堆栈而损坏

    在 .NET 中,不处理 StackOverflowException 不是因为它在技术上是不可能的,而是因为开发人员决定这样做。在 Windows 上,堆栈溢出会引发 STATUS_STACK_OVERFLOW (0xC00000FD) SEH 异常。CLR 捕获 SEH 异常,如果它看到此代码,则强制终止进程(使用默认选项加载)。同时,出于某种原因,更危险的 Access Violation .NET 允许处理。

    事实证明,当运行其他人的代码时,即使在单独的线程中,即使在单独的域中,我们也无法访问其源代码,我们仍然会失败,而在 .NET 中,这种类型无法防止这种情况发生例外?

    仅通过 .NET 是不可能的。然而,在非托管代码中,实际上几乎没有什么可写的。

    解决此问题的一种方法是创建一个自定义非托管 DLL,其唯一目的是处理 SEH 异常并将其代码更改为 CLR“不会吓到”的代码(具有未知代码的 SEH 异常由 CLR 转换为一个可以处理的 SEHException)。在 C# 应用程序中,加载 DLL,设置向量异常处理程序,并使用 SetThreadStackGuarantee 函数增加保留堆栈区域的大小。

    当然,这并不能完全恢复堆栈,也就是说,您可以再次在同一个线程中捕获堆栈溢出并处理它。但是如果你只是让线程终止并忘记它,那没关系:新创建的线程已经有一个有效的堆栈。

    例如,让我们使用以下代码创建一个 C++ DLL:

    #include <malloc.h>
    #include <windows.h>
    
    #ifdef __cplusplus
    extern "C"{
    #endif
    
    __declspec(dllexport) LONG WINAPI fnCrashHandler(LPEXCEPTION_POINTERS pExceptionInfo)
    {   
    
        if(pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_STACK_OVERFLOW){
            pExceptionInfo->ExceptionRecord->ExceptionCode = 0x1234;
        }
    
        return EXCEPTION_CONTINUE_SEARCH;
    }
    
    #ifdef __cplusplus
    }
    #endif
    

    我们将其命名为 CrashHandler.dll,并将其放在程序所在的目录中。然后在 C# 中,您可以像这样处理堆栈溢出:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    using System.Runtime.InteropServices;
    
    namespace ConsoleTest
    {
         class Program
        {                
    
            [DllImport("kernel32.dll")]        
            public static extern IntPtr AddVectoredExceptionHandler(
                uint FirstHandler,
                IntPtr VectoredHandler
            );                       
    
            [DllImport("kernel32.dll")]
            public static extern int SetThreadStackGuarantee(  ref uint StackSizeInBytes);         
    
            [DllImport("kernel32.dll")]
            public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);
    
            [DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true)]
            public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    
            static void Recursive()
            {
                Recursive();
            }
    
            static void Test()
            {            
    
                //увеличим размер зарезервированной области стека (30 KB должно быть достаточно)
                uint size = 30000; 
                SetThreadStackGuarantee(ref size);   
    
                try
                {
                    Recursive();
                }
                catch (SEHException)
                {
                    Console.WriteLine("SEHException. Code: 0x" + Marshal.GetExceptionCode().ToString("X"));                
                }
    
            }        
    
            static void Main(string[] args)
            {
                //добавим обработчик исключений
                IntPtr h = LoadLibrary("CrashHandler.dll");
                IntPtr fnAddress = GetProcAddress(h, "_fnCrashHandler@4"); //декорированное имя функции по правилам stdcall           
                AddVectoredExceptionHandler(1, fnAddress);
    
                //запустим поток
                Thread thread = new Thread(Test);
                thread.Start();
                thread.Join();            
    
                Console.WriteLine("Press any key...");
                Console.ReadKey();
            }
    
        }
    }
    

    笔记。非托管 DLL 和应用程序的目标体系结构必须匹配。对于 AnyCPU 应用程序,您需要为每个体系结构提供多个非托管 DLL,并根据应用程序的当前体系结构加载正确的 DLL。

    • 4

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    是否可以在 C++ 中继承类 <---> 结构?

    • 2 个回答
  • Marko Smith

    这种神经网络架构适合文本分类吗?

    • 1 个回答
  • Marko Smith

    为什么分配的工作方式不同?

    • 3 个回答
  • Marko Smith

    控制台中的光标坐标

    • 1 个回答
  • Marko Smith

    如何在 C++ 中删除类的实例?

    • 4 个回答
  • Marko Smith

    点是否属于线段的问题

    • 2 个回答
  • Marko Smith

    json结构错误

    • 1 个回答
  • Marko Smith

    ServiceWorker 中的“获取”事件

    • 1 个回答
  • Marko Smith

    c ++控制台应用程序exe文件[重复]

    • 1 个回答
  • Marko Smith

    按多列从sql表中选择

    • 1 个回答
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Suvitruf - Andrei Apanasik 什么是空? 2020-08-21 01:48:09 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5