逼近Windows限制:单个进程最大线&系统最大进程

12 篇文章 0 订阅
8 篇文章 0 订阅

       首先讨论线程限制,因为每个活动进程至少有一个线程(终止的进程,但是由另一个进程拥有的句柄保持引用),所以进程的限制直接受到限制线程的上限的影响。

        与一些UNIX变体不同,Windows中的大多数资源没有编译到操作系统中的固定上限,而是根据我已经介绍过的基本操作系统资源来推测它们的临界值。例如,进程和线程需要物理内存,虚拟内存,内存池。所以可以创建的进程或线程的数量,由这些资源中最先无法满足的那个限制。

   进程和线程

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

        最后,一个进程包含一个或多个线程,这些线程实际执行进程中的代码(从实现角度,进程执行,真正执行的是线程),并且用内核线程对象表示。除了默认的初始线程(主线程)外,应用程序还会创建其他线程。例如:具有用户界面的进程通常创建工作者线程,以便主线程能够及时响应用户操作;希望利用多个处理器实现可伸缩性的应用程序。多线程能够使应用程序:发挥多核CPU的优势或在一个线程挂起时,仍然能够执行其他线程。

       线程的边界

        除了线程的基本信息(CPU寄存器状态,调度优先级,和资源使用统计信息)外,进程还会分配给线程地址空间给线程,称为堆栈。线程可以使用用来:储执函数参数,保持局部变量、函数返回地址等。这样,系统的虚拟内存就不会被不必要地浪费,只初始分配或提交了堆栈的一部分,其余部分则被保留。由于堆栈在内存中向下增长,系统将保护页面放置在堆栈内存之外,当访问该部分时,将触发额外内存的自动提交(称为堆栈扩展)。这张图显示了堆栈的提交区域是如何向下增长的,以及当堆栈扩展时保护页面是如何移动的,以32位地址空间为例(不是按比例绘制的):

可执行映像的PE结构体保存线程堆栈保留大小和初始地址空间大小。链接器默认为预留1MB和一个内存页(4K),但是开发人员可以通过在链接程序时更改PE值或在调用CreateThread时更改单个线程的PE值来修改这些参数。您可以使用Visual Studio附带的Dumpbin之类的工具来查看可执行文件的设置。下面是带有/headers选项的Dumpbin输出,用于新Visual Studio项目生成的可执行文件:

转换为十六进制,你可以看到堆栈预留的大小是1MB和初始的4 k。使用Sysinternals VMMap工具Attach这个过程并查看它的地址空间中,你可以清楚地看到一个线程栈的初始也,和一个保护页。和其余的保留堆栈内存:

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

32位进程的最大线程数目

即使线程没有代码段或数据段,并且整个地址空间都做为栈空间。一个32位进程使用默认的2GB地址空间最多可以创建2,048个线程。下面是运行在32位窗口上的Testlimit工具的输出,其中-t开关(创建线程):

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

      在连接时,使用了大地址空间选项连接Testlimit程序。这意味着如果它提供超过2 gb的地址空间(例如在32位系统上启动/ 3 gb / USERVA ini选项或其等价的BCD选项)。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的内存(xp及以前的系统,64位线程的初始堆栈是1MB)。因为每个32位线程都是以64位模式开始其生命周期的,并且在启动时使用的堆栈空间超过了一个页面,所以您通常会看到使用了至少16KB的64位堆栈。下面是一个32位线程的64位和32位堆栈的例子(标记为“Wow64”的是32位堆栈):

32位Testlimit能够在64位Windows上创建3204个线程,这正是您所期望的,因为每个线程都使用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(我在一个小系统上做了这个实验,以突出这个特殊的限制):

       注意不同的错误,这意味着这里的问题不是地址空间。事实上,64K堆栈应该允许大约32,000个线程(2GB/64K = 32,768)。这里的极限是多少?看看可能的候选人,包括commit和pool,不要给出任何线索,因为他们都低于自己的极限:

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

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

一个基本的内核堆栈在32位Windows上是12K,在64位Windows上是24K。14,225个线程需要170MB常驻可访问内存。这与Testlimit不运行时系统空闲内存的确切数量相对应:

一旦达到驻留可用内存限制,许多基本操作就会开始失败。例如,当我双击桌面的Internet Explorer快捷方式时,出现了以下错误:

正如预期的,当运行在64位Windows和256MB内存上时,Testlimit只能创建6600个线程——大约是它在32位Windows和256MB内存上创建的线程的一半——然后耗尽驻留的可用内存:

我前面说“基本”内核堆栈的原因是,执行图形或窗口函数的线程在执行第一个调用时得到一个“大”堆栈,该调用在32位Windows上为20K,在64位Windows上为48K。Testlimit的线程不调用任何这样的api,所以它们有基本的内核堆栈。

64位线程的限制

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

在本例中,正是初始线程堆栈提交导致系统耗尽虚拟内存和“分页文件太小”错误。一旦提交级别达到RAM的大小,创建线程的速度就会减慢,因为系统开始抖动,将之前创建的线程堆分页出来,为新线程堆腾出空间,并且必须扩展分页文件。当指定-n开关时,结果是相同的,因为线程具有相同的初始堆栈。

进程限制

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

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

如果一个进程相对于驻留可用内存的惟一成本是内核模式线程堆栈,那么Testlimit将能够在2GB系统上创建超过8400个线程。当Testlimit不运行时,系统上驻留的可用内存为1.9GB:

将使用的驻留内存Testlimit的数量(1.9GB)除以它创建的进程的数量(8,400),每个进程产生230K的驻留内存。由于64位内核堆栈是24K,因此大约有206K没有计算在内。其余的消耗从何而来?创建进程时,Windows保留足够的物理内存来容纳进程的最小工作集大小。作为保证,无论如何,必须有足够的可用物理内存来容纳足够的数据来满足其最小工作集。默认的工作集大小是200 kb,事实很明显当你添加的最小工作集列process Explorer的显示:

剩下的大约6K是为表示进程分配的额外非分页内存占用的驻留可用内存。32位Windows上的进程将使用更少的驻留内存,因为它的内核模式线程堆栈更小。

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

在这里你可以看到系统的内核调试器报告不仅承诺限制被命中,但这已经有成千上万的内存分配失败,虚拟和分页池的分配,耗尽的提交限制(系统提交限制实际上是打几次作为分页文件了,然后发展到提高限制):

Testlimit运行之前的基线承诺约为1.5GB,因此线程消耗了约8GB的已提交内存。因此,每个进程大约消耗8GB/6,600,即1.2MB。内核调试器的!vm命令的输出,显示了每个活动进程分配的私有内存,确认了计算结果:

前面描述的初始线程堆栈承诺影响很小,其余的影响来自进程地址空间数据结构、页表条目、句柄表、进程和线程对象以及进程初始化时创建的私有数据所需的内存。

有多少线程和进程是足够的?
“Windows支持多少线程?”以及“在Windows上可以同时运行多少个进程?”“视情况而定。除了线程指定堆栈大小和进程指定最小工作集的方式的细微差别之外,在任何特定系统上决定答案的两个主要因素还包括物理内存的数量和系统提交限制。在任何情况下,创建足够多的线程或进程以接近这些限制的应用程序都应该重新考虑它们的设计,因为几乎总是有其他方法可以用合理的数量来实现相同的目标。例如,一个可伸缩应用程序的总体目标是保持运行的线程数量等于cpu的个数(NUMA改变这种考虑每个节点cpu)和一个实现这一方法是切换使用同步I / O使用异步I / O和依赖于I / O完成端口来帮助匹配运行线程的数量的cpu数量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值