0

0

耗时两天,优化失败

星夢妙者

星夢妙者

发布时间:2025-06-25 09:50:01

|

246人浏览过

|

来源于php中文网

原创

你好,我是雨乐!

在上一篇文章基于线程池的线上服务性能优化中,我们提到了使用线程池进行某个业务功能优化,在上线之后,实时性提高了大概24-30倍样子,基本能够满足实时性要求。在正常运行了几天之后,突然收到了报警,提示popen失败,于是打开了日志,发现有如下提示:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">popen file failed, id: abc url: http:xxx.txt errno: 12</code>

于是,开始查看错误提示,如下:

耗时两天,优化失败

看来是内存不足,于是,通过free命令查看所在机器的内存信息,如下:

耗时两天,优化失败

可用内存还有2.7G,不至于分配失败呀。

问题定位

看到popen()提示内存分配失败,首先就开始怀疑是否是wget使用有问题,但经过仔细研究之后,发现问题跟该命令无关,这是因为wget仅仅是将文件下载到本地,并不会占用过多的内存

既然问题与wget命令本身无关,那么问题苗头就指向popen本身了,于是在搜索引擎中搜索popen ENOMEM,其中有一条与本次遇到的问题很像,如下:

耗时两天,优化失败

通过该文内容,得到了一个很重要的信息,那就是popen的实现是fork+execve。熟悉fork()的开发人员都知道,fork()以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。既然fork()会生成父进程的一个副本,那么父进程所占用的所有资源,在子进程中也就会被拷贝一份。换句话说,fork()函数为clone父进程的所有资源,这样就能理解为什么当可用内存小于50%的时候,popen()会失败。

于是,为了验证文章的内容是否与本次遇到的问题一致,在本地写了一个简单的测试用例,测试代码中仅仅包含popen()函数,编译,然后使用starce ./test之后,输出如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">...futex(0x7ffdd648a69c, FUTEX_WAKE, 1)    = 0futex(0x7ffdd648a69c, FUTEX_WAKE_PRIVATE, 1) = 0pipe2([3, 4], O_CLOEXEC)                = 0clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f82fcab37d0) = 27437close(4)                                = 0fcntl(3, F_SETFD, 0)                    = 0exit_group(0) = ?...</code>

在上面的strace命令输出中,我们能看到一个很重要的函数那就是clone()(fork()函数会调用clone()),看来问题就在这。。。

源码分析

为了能够确认是否是因为popen()中的fork()所引起,于是找到了popen()函数的源码实现,如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">FILE *popen(const char *program, const char *type){    struct pid * volatile cur;    FILE *iop;    int pdes[2];    pid_t pid;    char *argp[] = {"sh", "-c", NULL, NULL};    if ((*type != 'r' && *type != 'w') || type[1] != '\0') {        errno = EINVAL;        return (NULL);    }    if ((cur = malloc(sizeof(struct pid))) == NULL)        return (NULL);    if (pipe(pdes) < 0) {        free(cur);        return (NULL);    }    switch (pid = fork()) {    case -1:   /* Error. */        (void)close(pdes[0]);        (void)close(pdes[1]);        free(cur);        return (NULL);        /* NOTREACHED */    case 0:    /* Child. */        {        struct pid *pcur;        /*         * We fork()'d, we got our own copy of the list, no         * contention.         */        for (pcur = pidlist; pcur; pcur = pcur->next)            close(fileno(pcur->fp));        if (*type == 'r') {            (void) close(pdes[0]);            if (pdes[1] != STDOUT_FILENO) {                (void)dup2(pdes[1], STDOUT_FILENO);                (void)close(pdes[1]);            }        } else {            (void)close(pdes[1]);            if (pdes[0] != STDIN_FILENO) {                (void)dup2(pdes[0], STDIN_FILENO);                (void)close(pdes[0]);            }        }        argp[2] = (char *)program;        execve(_PATH_BSHELL, argp, environ);        _exit(127);        /* NOTREACHED */        }    }    /* Parent; assume fdopen can't fail. */    if (*type == 'r') {        iop = fdopen(pdes[0], type);        (void)close(pdes[1]);    } else {        iop = fdopen(pdes[1], type);        (void)close(pdes[0]);    }    /* Link into list of file descriptors. */    cur->fp = iop;    cur->pid =  pid;    cur->next = pidlist;    pidlist = cur;    return (iop);}</code>

在上述代码中,我们可以看到popen中使用了fork()函数。当调用完fork()函数后,子进程获得父进程的数据空间、堆和栈,但是这是子进程单独拥有的,并不和父进程共享,因此修改子进程的变量不会影响父进程的变量。父进程和子进程共享正文段。进一步验证了我们之前的观点:由于fork()函数创建的子进程复制了一份父进程的资源,如果父进程内存占用过大,使得剩余内存资源不足以使得子进程进行拷贝的时候,那么popen()函数返回失败

问题解决

既然使用popen会存在fork()函数创建的子进程拷贝父进程资源的情况,那么有没有其它实现方法,能够使得子进程不对父进程的资源进行拷贝呢?

有道智云AI开放平台
有道智云AI开放平台

有道智云AI开放平台

下载

这就是vfork()函数!vfork()的父子进程是共享数据的,也就是说使用vfork()产生的子进程不会复制父进程的资源,而是与父进程共享同一份资源,所以在子程序中修改变量,父进程的变量也会被修改。既然可以使用vfork()能够解决此次遇到的问题,那么,也就可以使用vfork()函数来实现popen()函数的功能了,用以解决此次问题。

为了验证使用vfork()是否会调用clone,写了一个简单的代码,然后使用strace ./test命令,输出如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f64dcaac000write(1, "1\n", 21)                      = 2</code>

可见,用vfork就并没有调用clone(可能与内核版本有关,待再次验证)。

于是开始着手使用vfork()来优化代码,为了与libc中popen进行区分,在此,以vpopen()和vpclose()来实现之前popen()和pclose()函数的功能,代码如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">//#ifdef  OPEN_MAX//static long openmax = OPEN_MAX;//#elsestatic long openmax = 0;//#endif /* * If OPEN_MAX is indeterminate, we're not * guaranteed that this is adequate. */#define OPEN_MAX_GUESS 1024 long open_max(void){    if (openmax == 0) {      /* first time through */        errno = 0;        if ((openmax = sysconf(_SC_OPEN_MAX)) < 0) {           if (errno == 0)               openmax = OPEN_MAX_GUESS;    /* it's indeterminate */           else               printf("sysconf error for _SC_OPEN_MAX");        }    }     return(openmax);} static pid_t    *childpid = NULL;  /* ptr to array allocated at run-time */static int      maxfd;  /* from our open_max(), {Prog openmax} */ FILE *vpopen(const char* cmdstring, const char *type){    int pfd[2];    FILE *fp;    pid_t   pid;     if((type[0]!='r' && type[0]!='w')||type[1]!=0)    {        errno = EINVAL;        return(NULL);    }     if (childpid == NULL) {     /* first time through */                  /* allocate zeroed out array for child pids */          maxfd = open_max();          if ( (childpid = (pid_t *)calloc(maxfd, sizeof(pid_t))) == NULL)              return(NULL);      }     if(pipe(pfd)!=0)    {        return NULL;    }     if((pid = vfork())<0)    {        return(NULL);   /* errno set by fork() */      }    else if (pid == 0) {    /* child */        if (*type == 'r')        {            close(pfd[0]);              if (pfd[1] != STDOUT_FILENO) {                  dup2(pfd[1], STDOUT_FILENO);                  close(pfd[1]);              }                   }        else        {            close(pfd[1]);              if (pfd[0] != STDIN_FILENO) {                  dup2(pfd[0], STDIN_FILENO);                  close(pfd[0]);              }                   }         /* close all descriptors in childpid[] */          for (int i = 0; i < maxfd; i++)          if (childpid[ i ] > 0)              close(i);           execl("/bin/sh", "sh", "-c", cmdstring, (char *) 0);          _exit(127);         }     if (*type == 'r') {          close(pfd[1]);          if ( (fp = fdopen(pfd[0], type)) == NULL)              return(NULL);      } else {          close(pfd[0]);          if ( (fp = fdopen(pfd[1], type)) == NULL)              return(NULL);      }     childpid[fileno(fp)] = pid; /* remember child pid for this fd */      return(fp);     }  int vpclose(FILE *fp){    int     fd, stat;      pid_t   pid;       if (childpid == NULL)          return(-1);     /* popen() has never been called */       fd = fileno(fp);      if ( (pid = childpid[fd]) == 0)          return(-1);     /* fp wasn't opened by popen() */       childpid[fd] = 0;      if (fclose(fp) == EOF)          return(-1);       while (waitpid(pid, &stat, 0) < 0)          if (errno != EINTR)              return(-1); /* error other than EINTR from waitpid() */       return(stat);   /* return child's termination status */   }</code>

修改现有线上代码如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">std::string cmd = "wget -t 3 -c -r -nd -P /data1/data/ –delete-after -np -A .txt http://url.txt";auto fp = vpopen(cmd.str().c_str(), "r");if (!fp) {  return;}</code>

编译,运行,然后在线上灰度,开始焦急的等待,此时竟然希望该进程内存占用赶紧超过50%?。

耗时两天,优化失败

赶紧看了下日志,没有输出错误日志,再通过redis命令查询该订单是否已经被加载:

耗时两天,优化失败

一切正常,看来问题已经解决(至少目前来看?)

结语

在本次优化中,使用基于vfork()的vpopen()函数来提到之前的基于fork()实现的popen()函数。最重要的一个原因是使用fork()的popen(),在创建子进程的时候会进行资源复制,即使使用写时复制技术,如果没有足够的内存来复制父进程使用的内存,fork也会失败。而之所以采用vfork(),正是因为其创建的子进程与父进程共享同一份资源,省略了资源拷贝这一个过程,进而解决了此次遇到的内存不足的问题。

但是,正是因为vfork()与父进程共享一份资源,使用稍有不慎,就会导致意想不到的后果,因此在某些内核版本中已经将其标记为废弃(obsolescent),所以本次使用vfork()来实现仅仅是一个临时版本,先让线上功能能够正常使用,后续将继续优化该功能。

生命不息,优化不止!

好了,今天的文章就到这,我们下期见!

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

605

2023.08.10

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

605

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

常用的数据库软件
常用的数据库软件

常用的数据库软件有MySQL、Oracle、SQL Server、PostgreSQL、MongoDB、Redis、Cassandra、Hadoop、Spark和Amazon DynamoDB。更多关于数据库软件的内容详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1006

2023.11.02

内存数据库有哪些
内存数据库有哪些

内存数据库有Redis、Memcached、Apache Ignite、VoltDB、TimesTen、H2 Database、Aerospike、Oracle TimesTen In-Memory Database、SAP HANA和ache Cassandra。更多关于内存数据库相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

671

2023.11.14

mongodb和redis哪个读取速度快
mongodb和redis哪个读取速度快

redis 的读取速度比 mongodb 更快。原因包括:1. redis 使用简单的键值存储,而 mongodb 存储 json 格式的数据,需要解析和反序列化。2. redis 使用哈希表快速查找数据,而 mongodb 使用 b-tree 索引。因此,redis 在需要高性能读取操作的应用程序中是一个更好的选择。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

501

2024.04.02

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.5万人学习

Rust 教程
Rust 教程

共28课时 | 6.8万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号