Уникаючи зайнятого очікування в баш, без команди сну


19

Я знаю, що можу чекати, коли умова стане справжньою в баші, роблячи:

while true; do
  test_condition && break
  sleep 1
done

Але це створює 1 підпроцес при кожній ітерації (сон). Я міг би їх уникнути, роблячи:

while true; do
  test_condition && break
done

Але він використовує багато процесора (зайнятий в очікуванні). Щоб уникнути підпроцесів та зайнятого очікування, я придумав рішення нижче, але вважаю це некрасивим:

my_tmp_dir=$(mktemp -d --tmpdir=/tmp)    # Create a unique tmp dir for the fifo.
mkfifo $my_tmp_dir/fifo                  # Create an empty fifo for sleep by read.
exec 3<> $my_tmp_dir/fifo                # Open the fifo for reading and writing.

while true; do
  test_condition && break
  read -t 1 -u 3 var                     # Same as sleep 1, but without sub-process.
done

exec 3<&-                                # Closing the fifo.
rm $my_tmp_dir/fifo; rmdir $my_tmp_dir   # Cleanup, could be done in a trap.

Примітка: у загальному випадку я не можу просто використовувати read -t 1 varбез файлу, тому що він буде споживати stdin, і він не працюватиме, якщо stdin не є терміналом або трубою.

Чи можу я уникнути підпроцесів і зайнятого очікування більш елегантним способом?


1
trueє вбудованим і не створює підпроцес у bash. зайняте очікування завжди буде погано.
Йорданм

@joranm: ви праві true, питання оновлено.
jfg956

Чому б не без фіфо? Просто read -t 1 var.
ott--

@ott: ти маєш рацію, але це буде споживати stdin. Крім того, він не буде працювати, якщо stdin не є терміналом або трубою.
jfg956

Якщо проблема з ремонтом є проблемою, я б настійно запропонував би перейти з тим, sleepяк у першому прикладі. Другий, хоча це може спрацювати, нікому не буде легко коригуватись у майбутньому. Простий код також має більший потенціал для безпеки.
Kusalananda

Відповіді:


17

У нових версіях bash(принаймні v2) вбудовані модулі можуть завантажуватися (через enable -f filename commandname) під час виконання. Ряд таких вбудованих вбудованих матеріалів також розподіляється з джерелами базу та sleepє серед них. Наявність може, звичайно, відрізнятися від ОС до ОС (і навіть від машини до машини). Наприклад, у openSUSE ці вбудовані файли поширюються через пакет bash-loadables.

Редагувати: виправити ім'я пакета, додати мінімальну версію bash.


Ого, це те, що я шукаю, і я обов'язково дізнаюся щось про завантажене вбудоване: +1. Я спробую це, і все ж це найкраща відповідь.
jfg956

1
Це працює ! У програмі debian пакет є bash-вбудованими. Він включає лише джерела, і Makefile повинен бути відредагований, але мені вдалося встановити sleepяк вбудований. Спасибі.
jfg956

9

Створення безлічі підпроцесів - це погана річ у внутрішній петлі. Створення одного sleepпроцесу в секунду - це нормально. У цьому немає нічого поганого

while ! test_condition; do
  sleep 1
done

Якщо ви дійсно хочете уникнути зовнішнього процесу, вам не потрібно тримати фіфо відкритим.

my_tmpdir=$(mktemp -d)
trap 'rm -rf "$my_tmpdir"' 0
mkfifo "$my_tmpdir/f"

while ! test_condition; do
  read -t 1 <>"$my_tmpdir/f"
done

Ви маєте рацію про те, що процес в секунду є арахісом (але моє питання полягав у пошуку способу його видалення). Щодо коротшої версії, вона приємніша за мою, тому +1 (але я видалив так, mkdirяк це робиться mktemp(якщо ні, це умова гонки)). Також правда про те, while ! test_condition;що приємніше мого початкового рішення.
jfg956

7

Нещодавно у мене була потреба це зробити. Я придумав таку функцію, яка дозволить Башу спати вічно, не викликаючи жодної зовнішньої програми:

snore()
{
    local IFS
    [[ -n "${_snore_fd:-}" ]] || { exec {_snore_fd}<> <(:); } 2>/dev/null ||
    {
        # workaround for MacOS and similar systems
        local fifo
        fifo=$(mktemp -u)
        mkfifo -m 700 "$fifo"
        exec {_snore_fd}<>"$fifo"
        rm "$fifo"
    }
    read ${1:+-t "$1"} -u $_snore_fd || :
}

ПРИМІТКА. Раніше я розміщував версію цієї версії, яка б кожного разу відкривала та закривала дескриптор файлів, але я виявила, що в деяких системах, які роблять це сотні разів в секунду, згодом зачиниться. Таким чином, нове рішення зберігає дескриптор файлу між викликами функції. Bash все одно прибере при виході.

Це можна назвати так само, як / bin / сон, і він буде спати протягом потрібного часу. Називається без параметрів, він буде повісити назавжди.

snore 0.1  # sleeps for 0.1 seconds
snore 10   # sleeps for 10 seconds
snore      # sleeps forever

У моєму блозі є запис із зайвими подробицями


1
Відмінний запис у блозі. Однак я пішов туди, шукаючи пояснення, чому read -t 10 < <(:)повертається негайно, поки read -t 10 <> <(:)чекає цілих 10 секунд, але я все одно не отримую цього.
Амір

У read -t 10 <> <(:)чому полягає <>позиція?
CodeMedic

<> відкриває дескриптор файлу для читання та запису, навіть не дивлячись на те, що заміна базового процесу <(:) дозволяє лише читання. Цей злом викликає Linux, а саме Linux, припускати, що хтось може на нього написати, тому читання буде висіти в очікуванні на вхід, який ніколи не надійде. Це не зробить цього на системах BSD, і в такому випадку розпочнеться вирішення проблеми
болт

3

В ksh93або mksh, sleepце вбудована команда оболонки, тому альтернативою може бути використання цих оболонок замість bash.

zshтакож є zselectвбудований (завантажений zmodload zsh/zselect), який може спати протягом певної кількості сотих секунд zselect -t <n>.


2

Як сказав користувач yoi , якщо у вашому сценарії відкрито stdin , то замість сну 1 ви можете просто використовувати:

read -t 1 3<&- 3<&0 <&3

У Bash версії 4.1 і новіших ви можете використовувати число з плаваючою ланкою, наприклад read -t 0.3 ...

Якщо в скрипті stdin закритий (скрипт викликається my_script.sh < /dev/null &), вам потрібно використовувати інший відкритий дескриптор, який не видає висновок, коли виконується читання , наприклад. stdout :

read -t 1 <&1 3<&- 3<&0 <&3

Якщо в скрипті весь дескриптор закритий ( stdin , stdout , stderr ) (наприклад, тому що називається демоном), то вам потрібно знайти будь-який існуючий файл, який не видає результат:

read -t 1 </dev/tty10 3<&- 3<&0 <&3

read -t 1 3<&- 3<&0 <&3те саме, що read -t 0. Це просто читання з stdin з таймаутом.
Стефан Шазелас

1

Це працює як з оболонкою для входу, так і з неінтерактивної оболонки.

#!/bin/sh

# to avoid starting /bin/sleep each time we call sleep, 
# make our own using the read built in function
xsleep()
{
  read -t $1 -u 1
}

# usage
xsleep 3

Це також працювало на Mac OS X v10.12.6
b01

1
Це не рекомендується. Якщо декілька сценаріїв використовують це одночасно, вони отримують SIGSTOP'ed, коли всі вони намагаються прочитати stdin. Ваш стдін блокується, поки це чекає. Не використовуйте stdin для цього. Ви хочете нових дескрипторів файлів.
Нормалізувати

1
@Normadize Тут є ще одна відповідь ( unix.stackexchange.com/a/407383/147685 ), яка стосується проблеми використання безкоштовних дескрипторів файлів. Його мінімальна версія є read -t 10 <> <(:).
Амір


0

Невелике вдосконалення вищезгаданих рішень (на чому я грунтувався на цьому).

bash_sleep() {
    read -rt "${1?Specify sleep interval in seconds}" -u 1 <<<"" || :;
}

# sleep for 10 seconds
bash_sleep 10

Зменшилася потреба у фіфо і, отже, не потрібно робити прибирання.


1
Це не рекомендується. Якщо декілька сценаріїв використовують це одночасно, вони отримують SIGSTOP'ed, коли всі вони намагаються прочитати stdin. Ваш стдін блокується, поки це чекає. Не використовуйте stdin для цього. Ви хочете нових дескрипторів файлів.
Normadize

@Normadize Ніколи про це не думав; будь ласка, можете розробити або вказати мені на ресурс, де я можу прочитати більше про нього.
CodeMedic

@CodeMedic Тут є ще одна відповідь ( unix.stackexchange.com/a/407383/147685 ), яка стосується проблеми використання безкоштовних дескрипторів файлів. Його мінімальна версія є read -t 10 <> <(:).
Амір
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.