Python fork:';无法分配内存';如果进程消耗超过50%的可用性。记忆

Python fork:';无法分配内存';如果进程消耗超过50%的可用性。记忆,python,memory,fork,popen,allocation,Python,Memory,Fork,Popen,Allocation,我在Python中分叉进程时遇到了内存分配问题。我知道这个问题已经在这里的其他一些帖子中讨论过了,但是我在任何一个帖子中都找不到好的解决方案 以下是说明问题的示例脚本: import os import psutil import subprocess pid = os.getpid() this_proc = psutil.Process(pid) MAX_MEM = int(psutil.virtual_memory().free*1E-9) # in GB def consume_memo

我在Python中分叉进程时遇到了内存分配问题。我知道这个问题已经在这里的其他一些帖子中讨论过了,但是我在任何一个帖子中都找不到好的解决方案

以下是说明问题的示例脚本:

import os
import psutil
import subprocess
pid = os.getpid()
this_proc = psutil.Process(pid)
MAX_MEM = int(psutil.virtual_memory().free*1E-9) # in GB
def consume_memory(size):
    """ Size in GB """
    memory_consumer = []
    while get_mem_usage() < size:
        memory_consumer.append(" "*1000000) # Adding ~1MB
    return(memory_consumer)

def get_mem_usage():
    return(this_proc.memory_info()[0]/2.**30)

def get_free_mem():
    return(psutil.virtual_memory().free/2.**30)

if __name__ == "__main__":
    for i in range(1, MAX_MEM):
        consumer = consume_memory(i)
        mem_usage = get_mem_usage()
        print("\n## Memory usage %d/%d GB (%2d%%) ##" % (int(mem_usage), 
              MAX_MEM, int(mem_usage*100/MAX_MEM)))
        try:
            subprocess.call(['echo', '[OK] Fork worked.'])
        except OSError as e:
            print("[ERROR] Fork failed. Got OSError.")
            print(e)
        del consumer
请注意,在运行此测试时,我没有激活交换

解决这个问题似乎有两种选择:

  • 使用至少两倍于物理内存大小的交换
  • 更改Overmit_内存设置:echo 1>/proc/sys/vm/Overmit_内存
我在我的台式机上尝试了后者,上面的脚本完成时没有出现错误。 然而,在我正在开发的计算集群上,我不能使用这些选项中的任何一个

不幸的是,在消耗内存之前,提前分叉所需的进程也不是一个选项

有人对如何解决这个问题有其他建议吗

谢谢大家!

最好的


Leonhard

您所面临的问题实际上与Python无关,而且仅用Python也无法真正改变。按照注释中的建议提前启动分叉过程(executor)似乎是这个场景中最好、最干净的选择

不管是否使用Python,您正在处理Linux(或类似系统)如何创建新进程。您的父进程首先调用,这将创建一个新的子进程作为其自身的副本。当时它实际上并没有在其他地方复制自己(它使用写时复制),但是,它会检查是否有足够的空间可用,如果没有,则会将
errno
设置为
12:ENOMEM
->您看到的
OSError
异常

是的,允许虚拟机过度分配内存可以抑制出现此错误。。。如果你在孩子身上执行新的程序(最终也会变小)。它不必立即导致任何故障。但这听起来似乎有可能把问题踢得更远

增加内存(添加交换)。突破了限制,只要运行进程的两倍仍然适合可用内存,fork就可以成功。对于后续的exec,交换甚至不需要被利用

似乎还有一个选择,但看起来。。。肮脏的。还有另一个系统调用,它创建一个新进程,该进程最初与其父进程共享内存,其父进程的执行在该点暂停。这个新创建的子进程只能设置由
vWork
返回的变量,它可以
\u退出
exec
。因此,它不会通过任何Python接口公开,如果您尝试(我尝试过)使用
ctypes
将它直接加载到Python中,它会出错(我猜想,因为Python仍然会执行
vWork
之后提到的三个操作,然后在我可以
exec
子对象中的其他操作之前)

也就是说,您可以将整个
vWork
exec
委托给加载的共享对象。作为一个非常粗略的概念证明,我只是这样做了:

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

char run(char * const arg[]) {
    pid_t child;
    int wstatus;
    char ret_val = -1;

    child = vfork();
    if (child < 0) {
        printf("run: Failed to fork: %i\n", errno);
    } else if (child == 0) {
        printf("arg: %s\n", arg[0]);
        execv(arg[0], arg);
        _exit(-1);
    } else {
        child = waitpid(child, &wstatus, 0);
        if (WIFEXITED(wstatus))
            ret_val = WEXITSTATUS(wstatus);
    }
    return ret_val;
}
有了它,我仍然可以在3/4的可用内存已满的情况下分叉

理论上,它可以“正确”编写,也可以很好地包装以与Python代码集成,但它似乎是一个额外的选项。我还是会回到执行程序


我只是简单地浏览了
concurrent.futures.process
模块,但是一旦它产生了一个工作进程,它似乎在完成之前不会对它进行破坏,因此滥用现有的
ProcessPoolExecutor
可能是一个快速而廉价的选择。我在靠近脚本顶部(主要部分)的位置添加了以下内容:

然后提交
子流程。调用

proc = executor.submit(subprocess.call, ['echo', '[OK] Fork worked.'])
proc.result()  # can also collect the return value

您是否看过中建议的解决方案?TLDR:从一开始就启动一个小流程,然后可以根据需要启动其他子流程。是的,我考虑过这一点。然而,这需要在我的代码中进行一些更改。此外,消耗所有内存的大对象应该在工作人员之间共享。所以我希望在分叉工人时可以使用这个对象。我希望有一个更简单的方法来解决这个问题。非常感谢您提供的信息丰富的答案,并为我的迟来回复感到抱歉。我的管道中出现了另一个问题(分析一个包含蛋白质序列的18亿条边图),我不得不在最后几周集中精力解决这个问题。我一有时间就用vfork()测试你的建议。@Leo不用担心。实际上,我仍然建议采用执行人流程。:)也许在底层的廉价人的黑客甚至可以吗?这个
vWork()
示例确实有效,但边缘相当粗糙。。。(也就是说,如果使用的话,我认为用它制作一个合适的Python绑定模块是值得的。)仍然希望能够提供有用的提示。如果你发现缺了或不对劲,你可以再打我一次。
import ctypes
import os
import psutil
pid = os.getpid()
this_proc = psutil.Process(pid)
MAX_MEM = int(psutil.virtual_memory().free*1E-9) # in GB
def consume_memory(size):
    """ Size in GB """
    memory_consumer = []
    while get_mem_usage() < size:
        memory_consumer.append(" "*1000000) # Adding ~1MB
    return(memory_consumer)

def get_mem_usage():
    return(this_proc.memory_info()[0]/2.**30)

def get_free_mem():
    return(psutil.virtual_memory().free/2.**30)

if __name__ == "__main__":
    forker = ctypes.CDLL("forker.so", use_errno=True)
    for i in range(1, MAX_MEM):
        consumer = consume_memory(i)
        mem_usage = get_mem_usage()
        print("\n## Memory usage %d/%d GB (%2d%%) ##" % (int(mem_usage), 
              MAX_MEM, int(mem_usage*100/MAX_MEM)))
        try:
            cmd = [b"/bin/echo", b"[OK] Fork worked."]
            c_cmd = (ctypes.c_char_p * (len(cmd) + 1))()
            c_cmd[:] = cmd + [None]
            ret = forker.run(c_cmd)
            errno = ctypes.get_errno()
            if errno:
                raise OSError(errno, os.strerror(errno))
        except OSError as e:
            print("[ERROR] Fork failed. Got OSError.")
            print(e)
        del consumer
def nop():
    pass

executor = concurrent.futures.ProcessPoolExecutor(max_workers=1)
executor.submit(nop)  # start a worker process in the pool
proc = executor.submit(subprocess.call, ['echo', '[OK] Fork worked.'])
proc.result()  # can also collect the return value