Python分支:如果进程消耗的可用率超过50%,则“无法分配内存”。记忆

时间:2018-06-26 14:13:49

标签: 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_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

该脚本已在Arch Linux上使用Python 2.7和3.6进行了测试,并使用psutils跟踪内存使用情况。它逐渐增加了Python进程的内存使用量,并尝试使用subprocess.call()派生一个进程。如果超过50%的可用率,则分叉失败。内存被父进程占用。

## Memory usage 1/19 GB ( 5%) ##
[OK] Fork worked.

## Memory usage 2/19 GB (10%) ##
[OK] Fork worked.

## Memory usage 3/19 GB (15%) ##
[OK] Fork worked.

[...]

## Memory usage 9/19 GB (47%) ##
[OK] Fork worked.

## Memory usage 10/19 GB (52%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 11/19 GB (57%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 12/19 GB (63%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 13/19 GB (68%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

[...]

请注意,运行此测试时我没有激活交换。

似乎有两个选择可以解决此问题:

  • 使用至少两倍于物理内存大小的交换。
  • 更改overcommit_memory设置:echo 1> / proc / sys / vm / overcommit_memory

我在台式机上尝试了后者,并且以上脚本完成了,没有错误。 但是,在我正在使用的计算群集上,我无法使用任何这些选项。

不幸的是,也不要在消耗内存之前预先分叉所需的进程。

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

谢谢!

最佳

Leonhard

1 个答案:

答案 0 :(得分:1)

您面临的问题与Python并没有真正的关系,也没有单独使用Python可以做很多改变的事情。 mbrig在评论中建议先启动派生进程(执行程序),实际上似乎是此方案的最佳选择。

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

是的,允许VMS过量使用内存可以抑制此错误弹出……如果您在子程序中执行新程序(最终也会变小)。它不必引起任何立即的故障。但这听起来可能使问题进一步发展。

增长内存(添加交换)。突破极限,只要您的运行进程仍然适合可用内存两次,分叉就可以成功。使用后续执行程序,交换甚至不需要利用。

似乎还有一个选择,但是看起来...很脏。还有另一个系统调用vfork(),该系统调用创建一个新进程,该进程最初与其父进程共享内存,该父进程在那一点被挂起。这个新创建的子进程只能设置OSError返回的变量,可以vfork_exit进行设置。因此,它不会通过任何Python接口公开,并且如果您尝试(我确实)使用exec将其直接加载到Python中,则会导致段错误(我想是因为Python仍然会做其他事情,所以后面只提到了这三个动作) ctypes,然后我才能vfork孩子中的其他东西。

也就是说,您可以将整个execvfork委派到您加载的共享对象中。作为概念的非常粗略的证明,我就是这样做的:

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; } 之内和之内):

subprocess.call

这样,我仍然可以在报告的已填充的可用内存的3/4处分叉。

从理论上讲,它们都可以“适当地”编写,并且可以很好地包装以与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 模块,但是一旦它产生了一个工作进程,它似乎并没有掩盖它,所以也许滥用现有的concurrent.futures.process是一个快速而又轻松的过程。便宜的选择。我已将这些添加到脚本顶部(主要部分)附近:

ProcessPoolExecutor

然后将def nop(): pass executor = concurrent.futures.ProcessPoolExecutor(max_workers=1) executor.submit(nop) # start a worker process in the pool 提交给它:

subprocess.call