推动Windows的限制:进程和线程

本文探讨了Windows操作系统中线程和进程的限制,包括最大线程数和进程数,以及这些限制如何受物理内存和其他资源的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是我推行Windows系列的第四篇文章,探讨Windows基础资源的界限。这一次,我将讨论Windows上支持的最大线程数和进程数限制。我将简要介绍一下线程和进程之间的区别,调查线程限制,然后调查进程限制。我首先介绍了线程的限制,因为每个活动进程至少有一个线程(一个进程已经终止,但由另一个进程所拥有的句柄保持引用将不会有),所以进程的限制直接受上限限制线程。

与某些UNIX变体不同,Windows中的大多数资源没有编译到操作系统中的固定上限,而是基于我已经介绍的基本操作系统资源获取其限制。例如,进程和线程需要物理内存,虚拟内存和池内存,因此可以在给定的Windows系统上创建的进程或线程的数量最终由其中一个资源决定,具体取决于进程或者线程被创建并且首先命中哪个约束。因此,我建议您阅读前面的文章,因为我将指的是保留的内存,提交的内存,系统提交限制和我已经介绍的其他概念。这是整个推动限制系列的索引。虽然他们可以自立,

推动Windows的限制:物理内存

推动Windows的限制:虚拟内存

推动Windows的限制:分页和非分页池

推动Windows的限制:进程和线程

推动Windows的限制:把手

推动Windows的限制:USER和GDI对象 - 第1部分

推动Windows的限制:USER和GDI对象 - 第2部分

进程和线程

Windows进程本质上是承载可执行映像文件执行的容器。它用内核进程对象表示,Windows使用进程对象及其关联的数据结构来存储和跟踪关于图像执行的信息。例如,进程具有虚拟地址空间,该空间保存进程的私有和共享数据,并将可执行映像及其关联的DLL映射到该空间。Windows通过诊断工具记录进程使用资源进行记帐和查询,并将进程的引用注册到进程句柄表中的操作系统对象。进程使用称为令牌的安全上下文进行操作,该进程标识分配给该进程的用户帐户,帐户组和权限。

最后,一个进程包含一个或多个线程,这些线程实际上在进程中执行代码(从技术上说,进程不运行,线程也是这样),并由内核线程对象表示。除了默认的初始线程外,应用程序还创建线程有几个原因:具有用户界面的进程通常创建线程来执行工作,以便主线程保持对用户输入和窗口命令的响应; 希望利用多个处理器获得可伸缩性的应用程序,或希望在线程绑定时继续执行的应用程序,以等待同步I / O操作完成,同时也受益于多线程。

线程限制

除了关于线程的基本信息,包括CPU寄存器状态,调度优先级和资源使用情况,每个线程都有一部分进程地址空间分配给它,称为堆栈,线程在执行时可以用作临时存储程序代码传递函数参数,维护本地变量,保存函数返回地址。为了不浪费系统的虚拟内存,只有部分堆栈被初始分配或提交,剩下的只是保留。由于堆栈在内存中向下扩展,因此系统会在堆栈的提交部分之外放置防护页,以便在访问时触发附加内存的自动承诺(称为堆栈扩展)。该图显示了堆栈的提交区域如何增长,当堆栈扩展时,保护页面将移动,

图片

可执行映像的可移植可执行(PE)结构指定为线程堆栈保留并初始提交的地址空间量。链接器默认为1MB的保留,并提交一页(4K),但是开发人员可以通过在链接其程序时更改PE值或者调用CreateThread中的单个线程来覆盖这些值您可以使用Visual Studio附带的Dumpbin工具来查看可执行文件的设置。下面是Dumpbin的输出和新的Visual Studio项目生成的可执行文件的/ headers选项:

图片

从十六进制转换数字,你可以看到堆栈保留大小是1MB,初始提交是4K,使用新的Sysinternals VMMap工具附加到这个进程并查看它的地址空间,你可以清楚地看到一个线程堆栈的初始提交页面,一个保护页面,以及其余的保留堆栈内存:

图片

因为每个线程都占用了一部分进程的地址空间,所以进程对它们可以创建的线程数量有一个基本的限制,这个限制是由地址空间大小除以线程堆栈大小决定的。

32位线程限制

即使线程没有代码或数据,并且整个地址空间可以用于堆栈,具有默认2GB地址空间的32位进程也可以创建最多2,048个线程。以下是在32位Windows上运行Testlimit工具的输出,使用-t开关(创建线程)确认限制:

图片

同样,由于代码和初始堆已经使用了一部分地址空间,因此并不是所有2GB都可用于线程堆栈,因此创建的总线程数不能达到2,048的理论极限。

我将Testlimit可执行文件与大型地址空间感知选项链接起来,这意味着如果提供的地址空间超过2GB(例如,在使用/ 3GB或/ USERVA Boot.ini选项或其等效BCD引导的32位系统上Vista的选项和后来增加),它会使用它。32位进程在64位Windows上运行时会获得4GB地址空间,因此32位Testlimit在64位Windows上运行时会创建多少个线程?根据我们到目前为止的内容,答案应该大致为4096(4GB除以1MB),但实际上这个数字要小得多。这里是在64位Windows XP上运行的32位Testlimit:

图片

造成这种差异的原因是,在64位Windows上运行32位应用程序时,它实际上是一个64位进程,它代表32位线程执行64位代码,因此在那里是为每个线程保留的64位线程堆栈和32位线程堆栈区域。64位堆栈有256K的储备(除了Vista之前的系统,初始线程的64位堆栈是1MB)。由于每个32位线程以64位模式开始其生命周期,并且它在启动时使用的堆栈空间超过一页,因此您通常会看到提交的64位堆栈至少有16KB。这是一个32位线程的64位和32位堆栈的例子(标有“Wow64”的是32位堆栈):

图片

32位Testlimit能够在64位Windows上创建3,204个线程,因为每个线程都使用1MB + 256K的地址空间用于堆栈(除了Vista之前的版本(使用1MB + 1MB)之外, ,正是你所期望的。当我在64位Windows 7上运行32位Testlimit时,获得了不同的结果,但是:

图片

Windows XP结果和Windows 7结果之间的区别是由Windows Vista中引入的地址空间布局的随机性引起的,地址空间加载随机化(ASLR)导致了一些碎片。DLL加载,线程堆栈和堆放置的随机化有助于抵御恶意代码注入。从这个VMMap输出中可以看到,还有357MB的地址空间仍然可用,但是最大的空闲块只有128K的大小,小于32位堆栈所需的1MB空间:

图片

正如我提到的,开发人员可以覆盖默认的堆栈保留。这样做的一个原因是,当线程的堆栈使用总是远远小于默认的1MB时,避免浪费地址空间。Testlimit将其PE映像中的默认堆栈保留设置为64K,并且当您将-n开关与-t开关一起包含时,Testlimit将创建具有64K堆栈的线程。这是32位Windows XP系统上256MB RAM的输出(我在一个小系统上做了这个实验来突出这个特殊的限制):

图片

注意不同的错误,这意味着地址空间在这里不是问题。实际上,64K堆栈应该允许大约32,000个线程(2GB / 64K = 32,768)。这种情况下受到的限制是多少?看看可能的候选人,包括提交和共享,都没有提供任何线索,因为他们都低于他们的极限:

图片

这只是看看内核调试器中额外的内存信息,它揭示了正在被击中的阈值,已经耗尽的驻留可用内存:

图片

驻留可用内存是可以分配给必须保存在RAM中的数据或代码的物理内存。非分页池和非分页的驱动程序会对它进行计数,例如,对于设备I / O操作锁定在RAM中的内存也是如此。每个线程都有一个用户模式堆栈,这就是我一直在谈论的内容,但是它们也有一个内核模式堆栈,当它们以内核模式运行时,例如在执行系统调用时使用。当一个线程处于活动状态时,它的内核堆栈被锁定在内存中,以便线程可以在内核中执行不能分页错误的代码。

基本内核堆栈在32位Windows上是12K,在64位Windows上是24K。14,225个线程需要大约170MB的驻留可用内存,这对应于Testlimit未运行时系统上多少空闲的内存:

图片

一旦居民可用的内存限制被击中,许多基本的操作就开始失败。例如,这是我在桌面的Internet Explorer快捷方式上双击时遇到的错误:

图片

正如预期的那样,在256MB内存的64位Windows上运行时,Testlimit只能创建6600个线程,大约是在32位Windows上创建的256MB内存的一半,而不会耗尽驻留的可用内存:

图片

我之前说“基本”内核堆栈的原因是,执行图形或窗口函数的线程在执行32位Windows上的20K和64位Windows上的48K上的第一个调用时会获得“大”堆栈。Testlimit的线程不会调用任何这样的API,因此它们具有基本的内核堆栈。

64位线程限制

与32位线程一样,64位线程也为堆栈预留了1MB的预留空间,但64位进程的用户模式地址空间(8TB)大得多,所以地址空间不应该成为问题创造大量的线程。但是,居民可用内存显然仍然是潜在的限制因素。Testlimit(Testlimit64.exe)的64位版本能够在256MB 64位Windows XP系统上创建大约6,600个线程,而不使用-n开关,与32位版本创建的数量相同打到居民可用的内存限制。但是,在一个具有2GB内存的系统上,Testlimit64只能创建55,000个线程,远远低于如果驻留可用内存是限制器(2GB / 24K = 89,000)应该能够达到的数量:

图片

在这种情况下,初始线程堆栈提交会导致系统用尽虚拟内存,并且“分页文件太小”错误。一旦提交级别达到RAM的大小,线程创建的速度减慢到一个爬行,因为系统开始颠簸,调出早先创建的线程堆栈为新线程的堆栈腾出空间,并且页面文件必须展开。当指定-n开关时结果相同,因为线程具有相同的初始堆栈承诺。

过程限制

Windows支持的进程数显然必须少于线程数,因为每个进程有一个线程,进程本身会导致额外的资源使用。运行在2GB 64位Windows XP系统上的32位Testlimit创建了大约8,400个进程:

图片

内核调试器的外观显示它达到了驻留的可用内存限制:

图片

如果进程相对于驻留可用内存的唯一成本是内核模式线程堆栈,则Testlimit将能够在2GB系统上创建远远多于8,400个线程。Testlimit未运行时,此系统上驻留的可用内存量为1.9GB:

图片

将所使用的驻留内存数量(1.9GB)除以其创建的进程数量(8,400),即可获得每个进程230K的驻留内存。由于64位内核堆栈为24K,因此大约有206K的内核堆栈未被启用。其余的成本从哪里来?创建进程时,Windows将保留足够的物理内存以适应进程的最小工作集大小。这可以作为一个过程的保证,无论如何,将有足够的物理内存来存储足够的数据来满足其最小工作集。默认的工作集大小恰好是200KB,当您将最小工作集列添加到Process Explorer的显示时,这一点很明显

图片

其余大约6K是驻留可用内存,用于分配用于表示进程的额外的非可分页内存。在32位Windows上的进程将使用稍少的常驻内存,因为其内核模式线程堆栈较小。

对于用户模式线程堆栈,进程可以使用SetProcessWorkingSetSize函数覆盖其默认工作集大小Testlimit支持-n开关,当与-p结合使用时,Testlimit主进程的子进程将其工作集设置为尽可能最小,即80K。由于子进程必须运行以缩小其工作集,因此Testlimit在无法创建更多进程之后会休眠,然后再次尝试给子进程执行。在带有4GB RAM的Windows 7系统上使用-n开关执行的Testlimit达到了常驻可用内存以外的限制:系统提交限制:

图片

在这里你可以看到内核调试器不仅报告系统提交限制已经命中,而且在提交限制耗尽之后,已经有成千上万的内存分配失败,包括虚拟和分页池分配(系统提交限制是实际上在分页文件被填满之后打了几次,然后增长以提高限制):

图片

Testlimit之前的基准承诺约为1.5GB,所以线程已经消耗了大约8GB的承诺内存。因此每个进程消耗大约8GB / 6600或1.2MB。内核调试器的!vm命令(显示每个活动进程分配的私有内存)的输出证实了这一计算:

图片

前面介绍的初始线程堆栈承诺与来自进程地址空间数据结构,页表项,句柄表,进程和线程对象以及进程创建时的私有数据所需内存的其余部分的影响可以忽略不计初始化。

多少线程和进程是足够的?

因此,“Windows支持多少线程”和“在Windows上同时运行多少个进程”的问题的答案取决于。除了线程指定其堆栈大小和进程的方式的细微差别之外,还指定了其最小工作集,决定任何特定系统的答案的两个主要因素包括物理内存量和系统提交限制。无论如何,创建足够的线程或进程以接近这些限制的应用程序应该重新考虑它们的设计,因为几乎总是有多种替代方法来以合理的数量完成相同的目标。例如,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值