Изменить переменную в работающем скрипте Bash

У меня есть сценарий bash, который обрабатывает данные за несколько лет, поэтому на выполнение сценария может уйти неделя. Чтобы ускорить процесс, я использую многопоточность, запуская несколько экземпляров параллельно (каждый экземпляр = 1 день данных). Каждый экземпляр занимает 1 ЦП, поэтому я могу запускать столько экземпляров, сколько доступно ЦП. Поскольку я запускаю процесс на мощном сервере, которым я делюсь с другими, в какой-то момент у меня может быть больше или меньше доступных процессоров. Мой текущий сценарий:

#!/bin/bash
function waitpid {
   #Gather the gLABs PID background processes (Maximum processes in 
   #background as number of CPUs)
   NUMPIDS=`jobs -p|awk 'END {print NR}'`
   #A while is set because there seems to be a bug in bash that makes 
   #sometimes the "wait -n" command
   #exit even if none of provided PIDs have finished. If this happens, 
   #the while loops forces the 
   #script to wait until one of the processes is truly finished
   while [ ${NUMPIDS} -ge ${NUMCPUS} ]
   do
     #Wait for gLAB processes to finish
     PIDS="`jobs -p|awk -v ORS=" " '{print}'`"
     wait -n ${PIDS} >/dev/null 2>/dev/null
     NUMPIDS=`jobs -p|awk 'END {print NR}'`
   done
}
NUMPCUS=10
for(...) #Loop for each day
do
   day=... #Set current day variable
   #Command to execute, put in background
   gLAB_linux -input ${day}folder/${day}.input -output ${day)outfolder/${day}.output &        
   #Wait for any process to finish if NUMCPUS number of processes are running in background
   waitpid 
done

Поэтому мой вопрос: если этот скрипт запущен, могу ли я каким-либо образом изменить переменную NUMCPUS на любое значение (например, NUMCPUS=23) без остановки скрипта? Если возможно, я бы предпочел метод, который не требует чтения или записи в файл (мне нравится уменьшать количество временных файлов до 0, если это возможно). Я не возражаю, если это «хакерский» процесс, такой как метод, описанный в этот ответ. На самом деле, я пробовал в gdb те же команды, что и в этом ответе, но это не сработало, у меня были следующие ошибки в gdb (а также произошел сбой процесса):

(gdb) attach 23865
(gdb) call bind_variable("NUMCPUS",11,0)
'bind_variable' has unknown return type; cast the call to its declared return type
(gdb) call (int)bind_variable("NUMCPUS",11,0)
Program received signal SIGSEGV, Segmentation fault

EDIT1: Некоторые комментарии к сценарию:

  • gLAB_linux — это программа обработки с одним ядром, которая не знает о переменной NUMCPUS.
  • Каждое выполнение gLAB_linux занимает около 5 часов, поэтому скрипт bash большую часть времени спит внутри файла wait -n.
  • NUMCPUS должна быть локальной переменной для скрипта, так как параллельно может выполняться другой скрипт, подобный этому (изменение только параметров, заданных для gLAB_linux). Поэтому NUMCPUS не может быть переменной среды.
  • Единственный процесс, обращающийся к NUMCPUS, — это сценарий bash.

EDIT2: после ответа @Kamil я добавляю свое предложение по чтению из файла количества процессоров.

function waitpid {
   #Look if there is a file with new number of CPUs
   if [ -s "/tmp/numCPUs_$$.txt" ]
   then
     TMPVAR=$(awk '$1>0 {print "%d",$1} {exit}' "/tmp/numCPUs_$$.txt")
     if [ -n "${TMPVAR}" ]
     then
       NUMCPUS=${TMPVAR}
       echo "NUMCPUS=${TMPVAR}"
     fi
     rm -f "/tmp/numCPUs_$$.txt"
   fi

   #Gather the gLABs PID background processes (Maximum processes in 
   #background as number of CPUs)
   NUMPIDS=`jobs -p|awk 'END {print NR}'`
   #A while is set because there seems to be a bug in bash that makes 
   #sometimes the "wait -n" command
   #exit even if none of provided PIDs have finished. If this happens, 
   #the while loops forces the 
   #script to wait until one of the processes is truly finished
   while [ ${NUMPIDS} -ge ${NUMCPUS} ]
   do
     #Wait for gLAB processes to finish
     PIDS="`jobs -p|awk -v ORS=" " '{print}'`"
     wait -n ${PIDS} >/dev/null 2>/dev/null
     NUMPIDS=`jobs -p|awk 'END {print NR}'`
   done
}

person AwkMan    schedule 22.11.2018    source источник
comment
См. аргумент --limit в GNU parallel.   -  person choroba    schedule 22.11.2018
comment
а как насчет управления с сигналом?   -  person georgexsh    schedule 22.11.2018
comment
У каждого экземпляра будут разные входные и выходные файлы и папки, поэтому я не могу использовать параллельную команду bash. Я отредактировал вопрос. С сигналами проблема в том, что я не могу установить произвольное значение переменной   -  person AwkMan    schedule 22.11.2018
comment
@AwkMan чтение переменной из файла намного надежнее, чем взлом с помощью gdb, но я думаю, вы делаете это для удовольствия, а не для удачи! Кстати, parallel может читать строки cmd со стандартного ввода.   -  person georgexsh    schedule 22.11.2018
comment
Что касается ошибки wait -n: wait -n ожидает завершения следующего фонового задания; следующий как следующий из всех фоновых заданий текущего сеанса. PID, указанные после -n, игнорируются. Вы можете проверить это с помощью sleep 1 & sleep 9 & wait -n $!. $! — это PID sleep 9, но wait будет ждать только sleep 1.   -  person Socowi    schedule 22.11.2018
comment
@Socowi Ошибка, о которой я упоминал, заключается в том, что ожидание возвращается до завершения любого из процессов. Поскольку единственными процессами этого сеанса являются процессы gLAB_linux в фоновом режиме, ожидание должно возвращаться только после завершения любого gLAB_linux. Но я увидел, что у меня запущено более NUMCPU gLAB_linux (и с каждым разом все больше и больше), если я не добавлю время   -  person AwkMan    schedule 22.11.2018
comment
@georgexsh Да, я бы хотел взломать gdb, потому что это весело, а изменение переменной - это то, что мне редко нужно делать. Насчет параллелизма, даже я его использую, у меня все та же проблема, как мне динамически изменить количество процессоров в любой момент выполнения?   -  person AwkMan    schedule 22.11.2018
comment
@AwkMan parallel поддерживает чтение proc num из файла.   -  person georgexsh    schedule 22.11.2018


Ответы (2)


Лучше всего было бы изменить сценарий bash, чтобы он знал, что вы меняете значение. Изменение переменной среды внутри сеанса gdb - это просто навязчиво и в основном отбрасывает работу других разработчиков.

Ниже я использую файл с именем /tmp/signal_num_cpus. Если файл не существует, сценарий использует значение NUMCPUS. Если файл существует, он считывает его содержимое и соответствующим образом обновляет номер NUMCPUS, а затем печатает некоторое уведомление о том, что numcpus был изменен в файле. Если файл существует и не содержит допустимого числа (например, в предопределенном диапазоне или что-то в этом роде), он выводит в файл сообщение об ошибке. Другая сторона уведомляется о том, что все в порядке или произошло что-то плохое

#!/bin/bash

is_not_number() { 
    (( $1 != $1 )) 2>/dev/null
}

# global variable to hold the number of cpus with a default value
NUMCPUS=${NUMCPUS:=5}
# this will ideally execute on each access to NUMCPUS variable
# depending on content
get_num_cpus() { 
   # I tell others that NUMCPUS is a global variable and i expect it here
   declare -g NUMCPUS
   # I will use this filename to communicate
   declare -r file="/tmp/signal_num_cpus"
   # If the file exists and is a fifo...
   if [ -p "$file" ]; then
       local tmp
       # get file contents
       tmp=$(<"$file")
       if [ -z "$tmp" ]; then
           #empty is ignored
           :;
       elif is_not_number "$tmp"; then
           echo "Error reading a number from $file" >&2
           echo "error: not a number, please give me a number!" > "$file"
       else
           # If it is ok, update the NUMCPUS value
           NUMCPUS=$tmp
           echo "ok $NUMCPUS" > "$file"  # this will block until other side starts reading
       fi
   fi
   # last but not least, let's output it
   echo "$NUMCPUS"
}

# code duplication is the worst (ok, sometimes except for databases frameworks)
get_num_bg_jobs() {
    jobs -p | wc -l
}

waitpid() {
   while 
         (( $(get_num_bg_jobs) >= $(get_num_cpus) ))
   do
         wait -n
   done
}

# rest of the script

NUMPCUS=10
for(...) #Loop for each day
do
   day=... #Set current day variable
   #Command to execute, put in background
   gLAB_linux -input "${day}folder/${day}.input" -output "${day)outfolder/${day}.output" &        
   #Wait for any process to finish if NUMCPUS number of processes are running in background
   waitpid 
done

А скрипт изменения значения может выглядеть так:

#!/bin/bash

# shared context between scripts
declare -r file="/tmp/signal_num_cpus"

mkfifo "$file"

echo 1 > "$file" # this will block until other side will start reading

IFS= read -r line < "$file"

case "$line" in
ok*) 
     read _ numcpus <<<"$line"
     echo "the script changed the number of numcpus to $numcpus"
     ;;
*)
     echo "the script errored with $error"
     ;;
esac

rm "$file"

Метки:

  • правильный способ определения функции: func() { :; } Использование function func { } взято из ksh и поддерживается как расширение. Используйте func() {}
  • Удобно использовать арифметическое расширение (( ... )) для сравнения чисел и обработки.
  • Использование обратных кавычек ` для подстановки команд $( ... ) устарело.
person KamilCuk    schedule 22.11.2018
comment
Ваш метод использования файла мне кажется слишком сложным, особенно в отношении использования declare и mkfifo. Может быть, потому что я забыл уточнить некоторые вещи в своих вопросах (см. редактирование в моем вопросе). Я отредактирую (снова) свой вопрос для добавления моего предложения - person AwkMan; 22.11.2018
comment
declare просто для упрощения чтения, вы можете выбросить их все. mkfifo используется потому, что с их помощью легче синхронизировать процессы, можно использовать обычный файл + flock или вообще игнорировать блокировки. Если вы ищете несколько экземпляров сценария для запуска, добавьте уникальное имя к имени файла конфигурации в каждом сценарии, например, используя $$. - person KamilCuk; 22.11.2018

В главе 7.1 GNU Parallel 2018 рассказывается, как изменить количество потоков, используемых при работе https://zenodo.org/record/1146014

echo 50% > my_jobs
/usr/bin/time parallel -N0 --jobs my_jobs sleep 1 :::: num128 &
sleep 1
echo 0 > my_jobs
wait

Таким образом, вы просто помещаете аргумент для --jobs в my_jobs, и GNU Parallel будет читать его после каждого выполненного задания.

person Ole Tange    schedule 28.11.2018