Проблема, с которой вы сталкиваетесь, на самом деле не связана с Python, а также не является чем-то, что вы действительно могли бы изменить, используя только Python. Запуск процесса разветвления (исполнитель) заранее, как предложено mbrig в комментариях, действительно кажется лучшим и самым чистым вариантом для этого сценария.
Python или нет, вы имеете дело с тем, как Linux (или аналогичная система) создают новые процессы. Ваш родительский процесс сначала вызывает fork (2) , который создает новый дочерний процесс как копию самого себя. В то время он фактически не копирует себя в другое место (он использует копирование при записи), тем не менее, он проверяет, доступно ли достаточно места, и если нет, устанавливает errno
в 12: ENOMEM
-> исключение OSError
, которое вы ' Смотрю.
Да, разрешение VMS перегружать память может подавить появление этой ошибки ... и если вы выполняете новую программу (которая также будет меньше) в дочернем процессоре. Это не должно вызывать каких-либо немедленных сбоев. Но это звучит так, как будто вы можете решить проблему в будущем.
Увеличение памяти (добавление свопа). Увеличивает лимит и, пока ваш рабочий процесс в два раза умещается в доступной памяти, форк может быть успешным. С последующим exec своп даже не нужно будет использовать.
Кажется, есть еще один вариант, но выглядит ... грязно. Существует еще один системный вызов vfork () , который создает новый процесс, который первоначально разделяет память со своим родителем, выполнение которого в этот момент приостановлено. Этот недавно созданный дочерний процесс может устанавливать только переменную, возвращаемую vfork
, он может _exit
или exec
. Как таковой, он не предоставляется через какой-либо интерфейс Python, и если вы попытаетесь (я сделал) загрузить его непосредственно в Python, используя ctypes
, это приведет к segfault (я предполагаю, что Python все равно будет делать что-то другое, а не только три действия, упомянутые после * 1022). * и прежде чем я смог exec
что-то еще в ребенке).
Тем не менее, вы можете делегировать все vfork
и 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
):
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
С этим я все еще мог разветвляться на 3/4 доступной памяти, о которой сообщили, что она заполнена.
Теоретически все это может быть написано "правильно", а также красиво обернуто для хорошей интеграции с кодом Python, но пока это, кажется, еще одна дополнительная опция. Я бы все-таки вернулся к процессу исполнения.
Я только кратко просканировал модуль 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
на него:
proc = executor.submit(subprocess.call, ['echo', '[OK] Fork worked.'])
proc.result() # can also collect the return value