TIP: system errno 12 Cannot allocate memory

问题描述

程序中调用sytem执行gzip ‘file’脚本压缩文件,但是system返回-1,通过gdb查看,errno是12,就是Cannot allocate memory。

问题原因

system调用fork创建一个新的进程,然后在新的进程中调用execl函数执行脚本。这个errno 12就是fork引起的,用free命令查看了下内存,剩余不到400K,swap也用光了。而且,执行system的进程占用的内存很大。

解决方案

采用轻量级的执行脚本接口: posix_spawn。

相关资料

系统参数overcommit_memory与fork的关系

参考链接线程中fork提示 Cannot allocate memory 是否与线程库相关
系统参数:/proc/sys/vm/overcommit_memory
该文件指定了内核针对内存分配的策略,其值可以是0、1、2。

  • 0: 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。
  • 1: 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
  • 2: 表示内核允许分配超过所有物理内存和交换空间总和的内存(参照overcommit_ratio)。

2中考虑的是虚拟内存占用量,而不是物理内存,虚拟内存占用通常要比物理内存大很多,也就是说当设置2时,如果被fork进程的虚拟地址空间占用很大就可能导致失败的情况。
2中主要考虑的是物理内存,只有还有空闲的物理内存,而不管虚拟内存占用多少,应该都能分配到。
1则是另一种极端,如果需要保证一定不会分配失败的话,就设置1,当然当内存确是不足时,这也只是推迟了失败而已。
0是相对折中的方案,也是标准方式。

posix_spawn

博主提供了测试代码

#include <malloc.h>
#include <unistd.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

int main (void) {
  volatile void *bla = malloc (1024*1024*1024);
  int status;
  int pid;

  char* spawnedArgs[] = { "/bin/sleep", "2d", NULL };

  int f = posix_spawnp(&pid, spawnedArgs[0], NULL, NULL, spawnedArgs, NULL);

  printf("Pid: %d\nposix_spawn: %d\npid: %d\n---\n", getpid(), f, pid);
  scanf("scanf");

  wait(&status);
}

Linux 系统使用vfork代替fork

参考使用popen遭遇ENOMEM (Cannot allocate memory)的问题
博主写了一个vpopen函数

//#ifdef  OPEN_MAX
//static long openmax = OPEN_MAX;
//#else
static 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 */  

}

回收文件系统缓存

在文件读写时,Linux会做一个缓存,放在buffer中,在特定的时机才将缓存清理掉。Linux还提供了一个清理缓存的命令:

sync && echo 3 > /proc/sys/vm/drop_caches

当然,这个命令需要root权限。
执行的效果如下。
执行前:

[root@rhel149 3rd]$ free
             total       used       free     shared    buffers     cached
Mem:      32765960   32073544     692416          0    2722124   11436276
-/+ buffers/cache:   17915144   14850816
Swap:     33551712   26739792    6811920

执行后:

[root@rhel149 3rd]# free
             total       used       free     shared    buffers     cached
Mem:      32765960   16298020   16467940          0       3732     236400
-/+ buffers/cache:   16057888   16708072
Swap:     33551712   26739780    6811932

glibc 中的system和posix_spawn的区别

system的实现与posix_spawn很相似,最主要的区别应该是调用fork的区别。
在system中ia64平台上的调用是:

INLINE_SYSCALL (clone2, 6, CLONE_PARENT_SETTID | SIGCHLD, NULL, 0, \
          &pid, NULL, NULL)

在posix_spawn中ia64平台上的调用是:

#define ARCH_FORK() \
  INLINE_SYSCALL (clone2, 6,                              \
          CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD,        \
          NULL, 0, NULL, &THREAD_SELF->tid, NULL)

posix_spawn比system的clone2多了一个参数CLONE_CHILD_CLEARTID,这个参数的解释是:

Erase the child thread ID at the location ctid in child memory
when the child exits, and do a wakeup on the futex at that
address. The address involved may be changed by the
set_tid_address(2) system call. This is used by threading
libraries.

更简单的说明:

Clone option: Erase child thread ID in child memory space when child exits.

手册中关于set_tid_address的描述。

两个函数在glibc中的位置

system的实现在sysdeps/posix/system.c
posix_spawn在posix/spawn.c