首页 进程线程-基本概念
文章
取消

进程线程-基本概念

前言

  1. 进程线程的基本概念
    1. 什么是进程,线程,彼此有什么区别⭐⭐⭐⭐⭐
    2. 多进程、多线程的优缺点⭐⭐⭐⭐
    3. 什么时候用进程,什么时候用线程⭐⭐⭐
    4. 多进程、多线程同步(通讯)的方法⭐⭐⭐⭐⭐
    5. 进程的空间模型⭐⭐⭐⭐
    6. 进程线程的状态转换图 什么时候阻塞,什么时候就绪⭐⭐⭐
    7. 父进程、子进程的关系以及区别⭐⭐⭐⭐
    8. 什么是进程上下文、中断上下文⭐⭐
    9. 一个进程可以创建多少线程,和什么有关⭐⭐

      1.什么是进程,线程,彼此有什么区别

      1. 基本概念:

 1.进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
 2.线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。
 每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

2. 区别:

 1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。

 2.进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)

 3.进程是资源分配的最小单位,线程是CPU调度的最小单位

 4.系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销

 5.通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预

 6.进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。

 7.进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉

 8.进程适应于多核、多机分布;线程适用于多核

2. 多进程、多线程的优缺点

一. Linux下的多进程和多线程编程

在Linux环境下,多进程编程更为常见,而多线程编程相对较少使用。根据IBM的测试,切换线程上下文时,Windows比Linux更快一倍多。甚至在进出最快的锁方面(Windows 2000的critical section和Linux的pthread_mutex),Windows的性能要比Linux快五倍左右。这并不意味着Linux不好,而是在实际编程中,综合考虑后,Linux更适合高性能服务器的开发。但在多线程领域,Windows略优于Linux,这也是因为Unix家族主要从多进程出发,而Windows一开始就是多线程设计。

在UNIX/Linux环境下,多线程的使用通常不是必需的。多线程相对于多进程的性能更低,可以说多线程的主要优势在于成本较低。在UNIX环境中,多进程和多线程的调度开销没有明显差异,即UNIX的进程调度效率很高。另外,内存消耗方面,除了全局数据区外,内存已经相当便宜,因此大多数服务器都有足够的内存,这不是问题

可以将多进程类比为立体交通系统,虽然建设成本较高,上下坡时消耗一些燃料,但不容易拥堵。多线程则类似于平面交通系统,建设成本较低,但存在很多红绿灯,容易拥堵。举个例子,假设我们都开着跑车,燃料(主频)不是问题,也不怕上下坡,但担心拥堵。 (用空间换时间和用时间换空间)

高性能的交易服务器中间件,比如TUXEDO,通常支持多进程。实际测试表明,TUXEDO在性能和并发效率方面非常出色。TUXEDO是由贝尔实验室开发的,与UNIX有着深刻的联系,因此他们的意见在UNIX编程中具有很大的参考价值。

二. 进程和线程

  1. 进程的优点
  • 具有封闭性和可再现性。
  • 支持程序的并发执行和资源共享,提高系统效率和资源利用率。
    1. 进程的缺点
  • 进程间的切换速度较慢,相比于线程更为耗时。
  • 进程间的内存无法直接共享,通信相对复杂。
    1. 线程的优点
  • 节省系统资源,线程共享相同的地址空间,启动线程的开销远低于启动进程。 线程之间通信方便,因为它们共享进程的内存空间。

    (启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段.而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间)

    适用于多CPU系统,操作系统能够有效地分配线程到不同的CPU核心上。

  1. 线程的缺点
  • 需要保存线程状态,频繁的线程切换会占用大量机时。
  • 容易出现程序设计中的问题,尤其是线程同步。

三. 多进程

  1. 多进程的优点
  • 每个进程互相独立,一个子进程崩溃不会影响主程序的稳定性。
  • 可以通过增加CPU核心来扩充性能。
  • 可以减少线程锁和解锁的开销,提高性能。
  • 每个子进程都有独立的2GB地址空间和相关资源,总性能上限很高。
    1. 多进程的缺点
  • 逻辑控制相对复杂,需要与主程序进行交互。
  • 跨进程通信对于大数据传输不太方便,更适合小数据量传输和密集计算。
  • 多进程的调度开销较大。

四. 多线程

  1. 多线程的优点
  • 无需跨进程通信,线程之间共享内存和变量。
  • 简化程序逻辑和控制方式。
  • 总体资源消耗较少,相较于多进程更为高效。
    1. 多线程的缺点
  • 每个线程与主程序共享2GB地址空间,受限于地址空间大小。
  • 线程同步和锁问题可能复杂。
  • 一个线程的崩溃可能影响整个程序的稳定性。
  • 当线程数量增加到一定程度后,性能提升有限,且线程调度本身可能成为问题。

 在实际开发中,多进程和多线程通常结合使用,根据需要为每个CPU核心创建一个子进程,然后在每个子进程内使用多线程来处理数据。这种组合方法可以根据要求和资源来灵活选择,以实现方便满足需求,成本适中的目标。

3. 什么时候用进程,什么时候用线程

  1. 创建和销毁较频繁使用线程,因为创建进程花销大
  2. 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
  3. 安全稳定选进程;快速频繁选线程;

4. 多进程、多线程同步(通讯)的方法

进程、线程间通信的方式:

 进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。

1. 管道:  管道主要包括匿名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信

匿名管道PIPE:

  • 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
  • 只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
  • 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

命名管道FIFO:

  • FIFO可以在无关的进程之间交换数据
  • FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2. 系统IPC:

消息队列

 消息队列,是消息的链接表,存放在内核中。

一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息 少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;

特点:

  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量semaphore

 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

特点:

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  • 支持信号量组。

信号signal

 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存(Shared Memory)

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等

特点:

  • 共享内存是最快的一种IPC,因为进程是直接对内存进行存取
  • 因为多个进程可以同时操作,所以需要进行同步
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

3. 套接字SOCKET:

 socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。

线程间通信的方式:

  • 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;

  • 互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问

  • 信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。

  • 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作进程间通信的方式:

总结:

  1. 进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
  2. 线程间通信主要包括临界区、互斥量、信号量、事件

五.进程的空间模型

 Linux下使用虚拟内存空间给每一个进程,32位操作系统下,每个进程都有独立的4G虚拟内存空间。

其中包括:

  1. 内核区:用户代码不可见的区域,页表就存放在这个区域中。ps:页表的作用是将进程的虚拟地址映射到实际的物理内存地址,以实现虚拟内存的概念。

  2. 用户区:

  • 代码段:只可读,不可写,程序代码段。
  • 数据段:保存全局变量,静态变量的区域。
  • 堆区:就是动态内存,通过malloc,new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针。
  • 文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区。可以单独释放,不会产生内存碎片。
  • 栈区:用于维护函数调用的上下文空间,用ulimit -s 查看。一般默认为8M

六.进程线程的状态转换图 什么时候阻塞,什么时候就绪

 为了是参与并发执行的每个程序都能独立地运行,在操作系统中必须为之配置一个专门的数据结构——“进程控制块(Process Control Block,PCB)”。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。程序段、相关数据段、PCB三部分构成了进程实体(又称进程映像)。所谓创建进程,实质上是创建进程实体中的PCB;而撤销进程,实质上是撤销进程的PCB。

进程的特征:(1)动态性 (2)并发性 (3)独立性 (4)异步性

1.进程的三种基本状态

就绪状态ready:进程已经分配除CPU以外的所有必要资源,只要在获得CPU就可立刻执行。

执行状态running:进程已获得CPU,其程序正在执行的状态。

阻塞状态block:正在执行的进程由于发生某事件(如IO请求、申请缓冲区失败等)暂时无法继续执行的状态。

2.三种基本状态的转换

image-8

  • 运行——>就绪:

    1. 主要是进程占用CPU的时间过长,而系统分配给该进程占用CPU的时间是有限的;
    2. 在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出CPU,该进程便由执行状态转变为就绪状态。
  • 就绪——>运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU

  • 运行——>阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如发生了I/O请求

  • 阻塞——>就绪:进程所等待的事件已经发生,就进入就绪队列

以下两种状态是不可能发生的:

  • 阻塞——>运行:即使给阻塞进程分配CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取

  • 就绪——>阻塞:就绪态根本就没有执行,谈不上进入阻塞态。

3.五种基本状态及转换

image-9

(1)创建状态:①进程申请一个空白PCB;②向PCB中填写用于控制和管理进程的信息;③为该进程分配运行所需要的的资源;④将该进程转入就绪状态并插入就绪队列中

(2)终止状态:①等待操作系统进行善后处理(操作系统保留该进程的信息供其他的进程提取);②将该进程的PCB清零,并将将PCB控件返还系统

4.挂起操作、激活操作和进程状态的转换

4.1.挂起和阻塞的区别

  • 对系统资源占用不同:虽然都释放了CPU,但阻塞的进程仍处于内存中,而挂起的进程通过“对换”技术被换出到外存(磁盘)中。

  • 发生时机不同:阻塞一般在进程等待资源(IO资源、信号量等)时发生;而挂起是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘

  • 恢复时机不同:阻塞要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;被挂起的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活

4.2.挂起操作的目的

  • 终端用户的需要: 修改、检查进程
  • 父进程的需要:修改、协调子进程
  • 对换的需要:缓和内存
  • 负荷调节的需要:保证实时任务的执行

4.3状态转换图

image-10

七.父进程、子进程的关系以及区别

 父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

 父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?

 当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

 关于资源:子进程得到的是除了代码段是与父进程共享的以外,其他所有的都是得到父进程的一个副本,子进程的所有资源都继承父进程,得到父进程资源的副本,既然为副本,也就是说,二者并不共享地址空间。两个是单独的进程,继承了以后二者就没有什么关联了,子进程单独运行。(采用写时复制技术)

 关于文件描述符:继承父进程的文件描述符时,相当于调用了dup函数,父子进程共享文件表项,即共同操作同一个文件,一个进程修改了文件,另一个进程也知道此文件被修改了。

ps: dup操作 功能:复制文件描述符,重定向输入输出。 ```cpp 函数:

#include int dup(int oldfd); int dup2(int oldfd, int newfd);

返回值: 成功:dup函数返回当前系统可用的最小整数值。 dup2函数返回第一个不小于newfd的整数值,分两种情况: 1. 如果newfd已经打开,则先将其关闭,再复制文件描述符; 2. 如果newfd等于oldfd,则dup2函数返回newfd,而不关闭它。 失败:dup和dup2函数均返回-1,并设置errno。 ``` 复制文件描述符后,新旧文件描述符的特点:

  • 使用dup或dup2复制文件描述符后,新文件描述符和旧文件描述符指向同一个文件,管道或网络连接,共享文件的锁定、读写位置和各项权限。
  • 当关闭新的文件描述符时,通过旧文件描述符仍可操作文件。
  • 当关闭旧的文件描述符时,通过新的文件描述符仍可操作文件。

八.什么是进程上下文、中断上下文

一.什么是内核态和用户态

内核态:在内核空间执行,通常是驱动程序,中断相关程序,内核调度程序,内存管理及其操作程序。

用户态:用户程序运行空间。

image

二.什么是进程上下文与中断上下文

1.进程上下文:

(1)进程上文:其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

(2)进程下文:其是指切换到内核态后执行的程序,即进程运行在内核空间的部分

2.中断上下文:

(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。

(2)中断下文:执行在内核空间的中断服务程序

三.为什么要进行不同之间状态的切换

 在现在操作系统中,内核功能模块运行在内核空间,而应用程序运行在用户空间。现代的CPU都具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,其所拥有的资源也不同;在较低的级别中将禁止使用某些处理器的资源。Linux系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态),这个级别几乎可以使用处理器的所有资源,而应用程序运行在较低级别(用户态),在这个级别的用户不能对硬件进行直接访问以及对内存的非授权访问。内核态和用户态有自己的内存映射,即自己的地址空间。

当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。我将其理解为保存用户空间状态是上文,切换后在内核态执行的程序是下文。

四.什么情况下进行用户态到内核态的切换

1.进程上下文主要是异常处理程序和内核线程内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。

2.中断上下文是由于硬件发生中断时会触发中断信号请求,请求系统处理中断,执行中断服务子程序。

五.中断上下文代码中注意事项

 运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。所以中断处理程序代码要受到一些限制,在中断代码中不能出现实现下面功能的代码:

(1)睡眠或者放弃CPU

 因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。牢记:中断服务子程序一定不能睡眠(或者阻塞)。

(2)尝试获得信号量

 如果获得不到信号量,代码就会睡眠,导致(1)中的结果。

(3)执行耗时的任务

 中断处理应该尽可能快,因为如果一个处理程序是IRQF_DISABLED类型,他执行的时候会禁止所有本地中断线,而内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。中断处理程序的任务尽可能放在中断下半部执行。

(4)访问用户空间的虚拟地址  因为中断运行在内核空间。

九.一个进程可以创建多少线程,和什么有关

这取决于什么操作系统,多少位操作系统

Linux 系统。

 在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址 空间的范围也不同。比如最常⻅的 32 位和 64 位系统,如下所示:

image-1

通过这里可以看出:

  • 32 位系统的内核空间占用 1G ,位于最高处,剩下的 3G 是用户空间;
  • 64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

一个进程最多可以创建多少个线程?

 这个问题跟两个东西有关系:

  • 进程的虚拟内存空间上限,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
  • 系统参数限制,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。 我们先看看,在进程里创建一个线程需要消耗多少虚拟内存大小?

 我们可以执行 ulimit -a 这条命令,查看进程创建线程时默认分配的栈空间大小,比如我这台服务器默认分配给线程的栈空间大小为 8M。

image-2

 在前面我们知道,在 32 位 Linux 系统里,一个进程的虚拟空间是 4G,内核分走了1G,留给用户用的只有 3G。

那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。

如果想使得进程创建上千个线程,那么我们可以调整创建线程时分配的栈空间大小,比如调整为 512k:

$ ulimit -s 512

说完 32 位系统的情况,我们来看看 64 位系统里,一个进程能创建多少线程呢?

我的测试服务器的配置:

  • 64 位系统;
  • 2G 物理内存;
  • 单核 CPU。
  • 64 位系统意味着用户空间的虚拟内存最大值是 128T,这个数值是很大的,如果按创建一个线程需占用 10M 栈空间的情况来算,那么理论上可以创建 128T/10M 个线程,也就是 1000多万个线程,有点魔幻!

所以按 64 位系统的虚拟内存大小,理论上可以创建无数个线程。

事实上,肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。

比如下面这三个内核参数的大小,都会影响创建线程的上限:

  • /proc/sys/kernel/threads-max,表示系统支持的最大线程数,默认值是 14553;
  • /proc/sys/kernel/pid_max,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 32768;
  • /proc/sys/vm/max_map_count,表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是 65530。

那接下针对我的测试服务器的配置,看下一个进程最多能创建多少个线程呢?

我在这台服务器跑了前面的程序,其结果如下:

image-3

可以看到,创建了 14374 个线程后,就无法在创建了,而且报错是因为资源的限制。

前面我提到的 threads-max 内核参数,它是限制系统里最大线程数,默认值是 14553。

我们可以运行那个测试线程数的程序后,看下当前系统的线程数是多少,可以通过 top -H 查看。

image-4

左上角的 Threads 的数量显示是 14553,与 threads-max 内核参数的值相同,所以我们可以认为是因为这个参数导致无法继续创建线程。

那么,我们可以把 threads-max 参数设置成 99999:

echo 99999 > /proc/sys/kernel/threads-max

设置完 threads-max 参数后,我们重新跑测试线程数的程序,运行后结果如下图:

image-5

可以看到,当进程创建了 32326 个线程后,就无法继续创建里,且报错是无法继续申请内存。

此时的上限个数很接近 pid_max 内核参数的默认值(32768),那么我们可以尝试将这个参数设置为 99999:

echo 99999 > /proc/sys/kernel/pid_max

设置完 pid_max 参数后,继续跑测试线程数的程序,运行后结果创建线程的个数还是一样卡在了 32768 了。

当时我也挺疑惑的,明明 pid_max 已经调整大后,为什么线程个数还是上不去呢?

后面经过查阅资料发现,max_map_count 这个内核参数也是需要调大的,但是它的数值与最大线程数之间有什么关系,我也不太明白,只是知道它的值是会限制创建线程个数的上限。

然后,我把 max_map_count 内核参数也设置成后 99999:

echo 99999 > /proc/sys/kernel/max_map_count

继续跑测试线程数的程序,结果如下图:

image-6

当创建差不多 5 万个线程后,我的服务器就卡住不动了,CPU 都已经被占满了,毕竟这个是单核 CPU,所以现在是 CPU 的瓶颈了。

我只有这台服务器,如果你们有性能更强的服务器来测试的话,有兴趣的小伙伴可以去测试下。

接下来,我们换个思路测试下,把创建线程时分配的栈空间调大,比如调大为 100M,在大就会创建线程失败。

ulimit -s 1024000

设置完后,跑测试线程的程序,其结果如下:

image-7

总共创建了 26390 个线程,然后就无法继续创建了,而且该进程的虚拟内存空间已经高达 25T,要知道这台服务器的物理内存才 2G。

为什么物理内存只有 2G,进程的虚拟内存却可以使用 25T 呢?

因为虚拟内存并不是全部都映射到物理内存的,程序是有局部性的特性,也就是某一个时间只会执行部分代码,所以只需要映射这部分程序就好。

你可以从上面那个 top 的截图看到,虽然进程虚拟空间很大,但是物理内存(RES)只有使用了 400 多M。

好了,简单总结下:

32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。

64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

本文由作者按照 CC BY 4.0 进行授权