广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何解决 Linux 下无法创建新线程并提示 errno 12 的问题?

在 Linux 环境下使用 pthread 库编写多线程程序时,偶尔会碰到 pthread_create 返回错误码 12 的情况。错误信息一般显示为 Can't create a new thread,而 errno 12 对应的宏是 ENOMEM,意思是内存不足。但这里的“内存不足”并不一定表示物理内存真的被吃光了,它可能指向线程栈的虚拟内存分配失败、进程地址空间耗尽,或者系统对线程数量的限制已经触顶。

如何解决 Linux 下无法创建新线程并提示 errno 12 的问题?

errno 12 的错误本质:不是简单的内存耗尽

pthread_create 函数在成功时返回 0,失败时返回错误编号而不是设置 errno。很多开发者在调用 perror 或 strerror 时看到 Cannot allocate memory,就会下意识去查 free 命令,结果发现物理内存还有不少剩余。这种判断偏差源于对错误码 12 的误解。ENOMEM 在 pthread_create 的上下文中表示“无法为线程分配所需的内存资源”,其中线程栈是最主要的分配对象。Linux 下每个线程默认会获得一块独立的栈空间,通常为 8 MB(32 位系统可能为 2 MB)。这块栈空间属于进程的虚拟地址空间,创建线程时内核会先为栈保留地址范围,再按需映射物理页。如果虚拟地址空间剩余不足,就会直接返回 ENOMEM,即使物理内存充足也无济于事。

另一个容易忽略的点是,线程创建还会涉及内核数据结构,比如 task_struct、内核栈和线程本地存储 TLS 的分配。这些内核对象虽然占用物理内存不大,但当线程数量达到系统上限或者 cgroup 限制时,内核同样会返回 ENOMEM。因此 errno 12 更像一个笼统的资源分配失败信号,定位它需要同时检查用户态虚拟地址空间、系统限制和 cgroup 配额。

下面的 C 代码演示了如何捕获 pthread_create 的返回错误码:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
void* worker(void* arg) {
    return NULL;
}
int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, worker, NULL);
    if (ret != 0) {
        printf("pthread_create failed: %s (errno %d)\n", strerror(ret), ret);
        return 1;
    }
    pthread_join(tid, NULL);
    return 0;
}

这段代码在正常情况下能成功创建线程,但如果系统资源已经触顶,就会打印 pthread_create failed: Cannot allocate memory (errno 12),从而帮助确认错误码。

触发 errno 12 的四个主要原因

要彻底解决线程创建失败,必须弄清楚哪个环节最先耗尽。第一个原因是进程虚拟地址空间不足。32 位进程只有 4 GB 地址空间,其中用户态通常约 3 GB。如果默认线程栈是 8 MB,创建三百多个线程就可能把地址空间占满,哪怕物理内存还剩很多。64 位进程地址空间巨大,一般不会触发这个限制,但如果线程栈被手动调得非常大,比如 1 GB,也会很快耗尽。

第二个原因是系统级线程数上限。Linux 通过 /proc/sys/kernel/threads-max 控制整个系统可以创建的线程总数,超过这个数值后 pthread_create 会返回 EAGAIN 或 ENOMEM。此外,每个用户还有进程数限制,可以通过 ulimit -u 查看。一个线程在内核中也算一个轻量级进程,所以用户进程数限制同样作用于线程。第三个原因是 cgroup 的 pids.max 限制。在容器环境中,cgroup 会限制任务数量,达到上限后新线程无法创建,错误码同样是 12。

第四个原因是物理内存与交换空间真正不足。虽然这种情况相对少见,但如果线程栈的物理页被大量使用,并且系统没有足够的可回收内存,内核可能会在缺页分配时失败。此时 free 命令会显示可用内存极低,同时伴随 OOM Killer 活动。理解这四个原因后,就可以有针对性地排查。

从命令行和 /proc 文件系统快速定位

当程序报出 errno 12 时,首先应该记录失败瞬间的系统状态。运行 free -h 查看物理内存和交换分区使用情况。如果可用内存远大于线程数量乘以栈大小,就可以初步排除物理内存耗尽。接着执行 ulimit -a 查看当前 shell 的用户进程数限制,关注 max user processes 一项。如果这个值很小,比如 1024,而程序已经创建了接近这个数量的线程,那么就是用户级限制导致的失败。

系统全局线程上限可以通过 cat /proc/sys/kernel/threads-max 获取。该值通常很大,但在某些精简内核或容器中可能被调低。如果怀疑虚拟地址空间不足,可以查看进程的内存映射,使用 pmap -x 进程号 或者读取 /proc/进程号/maps,观察线程栈区域的分布。对于 32 位进程,如果映射区域接近 3 GB,基本可以判定地址空间耗尽。在 cgroup v2 环境中,查看 /sys/fs/cgroup/pids.max 能确认容器是否设置了 pids 配额。

下面的命令组合可以一次性收集关键信息:

free -h
ulimit -u
cat /proc/sys/kernel/threads-max
cat /sys/fs/cgroup/pids.max
pmap -x $(pgrep your_program) | tail -n 20

根据输出结果,如果物理内存充足但 ulimit -u 数值较小,就应该调整用户进程数限制;如果 pmap 显示大量 8 MB 线程栈堆叠,则要考虑缩小栈大小或改用线程池。

从栈大小、系统限制和线程模型入手解决

最直接的代码级优化是自定义线程栈大小。如果线程函数不需要深层递归,完全可以把默认 8 MB 降到 512 KB 甚至更小。使用 pthread_attr_setstacksize 可以精确控制每个线程的栈尺寸。下面的示例创建 1000 个线程,每个栈只分配 512 KB,比默认 8 MB 节省了大量虚拟地址空间:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#define THREAD_COUNT 1000
#define STACK_SIZE (512 * 1024)
void* worker(void* arg) {
    return NULL;
}
int main() {
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, STACK_SIZE);
    pthread_t tids[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; i++) {
        int ret = pthread_create(&tids[i], &attr, worker, NULL);
        if (ret != 0) {
            printf("pthread_create %d failed: %s\n", i, strerror(ret));
            return 1;
        }
    }
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }
    pthread_attr_destroy(&attr);
    return 0;
}

这段代码通过 pthread_attr_setstacksize 将每个线程栈设置为 512 KB,在 32 位系统中最多可以创建数千个线程。需要注意的是,栈大小必须是页面大小的整数倍,并且不能小于 PTHREAD_STACK_MIN,否则 pthread_create 会返回 EINVAL。

如果程序确实需要大量并发任务,更好的做法是使用线程池或异步 I/O,避免为每个任务创建独立线程。线程池预先创建固定数量的工作线程,通过任务队列复用它们,不仅可以避免 errno 12,还能降低上下文切换开销。对于网络服务器,还可以考虑使用 epoll 配合事件循环,进一步减少线程数量。

在系统层面,可以提升用户进程数限制。临时修改可以执行 ulimit -u 65535,当前 shell 及其子进程立即生效。永久生效需要编辑 /etc/security/limits.conf,添加类似下面的配置行:

* soft nproc 65535
* hard nproc 65535

如果问题由 cgroup pids.max 引起,需要调整容器配置或使用 systemd 的资源控制参数。对于物理内存确实紧张的场景,要么增加内存、扩大交换分区,要么优化程序减少内存占用。总之,errno 12 的修复需要结合具体触发条件,不能盲目增大线程栈或调高限制,否则可能掩盖真正的资源泄漏问题。

广告位 · 文章底部(后台-模块管理替换此区域)