Як зробити так, щоб читання та запис одного і того ж файлу в одному трубопроводі завжди «провалювався»?


9

Скажіть, у мене є такий сценарій:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

У ключовому рядку я читаю і записую той самий файл, tmpякий іноді виходить з ладу.

(Я читав це через умови перегонів, тому що процеси в трубопроводі виконуються паралельно, і я не розумію, чому - кожен headповинен брати дані з попереднього, чи не так? Це НЕ моє головне питання, але ви можете відповісти також.)

Коли я запускаю сценарій, він видає близько 200 рядків. Чи можна змусити цей скрипт виводити завжди 0 рядків (тож переадресація вводу / виводу tmpзавжди готується першою і тому дані завжди знищуються)? Щоб було зрозуміло, я маю на увазі зміни системних налаштувань, а не цього сценарію.

Дякуємо за ваші ідеї.

Відповіді:


2

Відповідь Жиля пояснює стан гонки. Я просто відповім на цю частину:

Чи є спосіб, щоб я змусив цей сценарій виводити завжди 0 рядків (тому перенаправлення вводу / виводу на tmp завжди готується першим і тому дані завжди знищуються)? Щоб було зрозуміло, я маю на увазі зміну системних налаштувань

IDK, якщо інструмент для цього вже існує, але я маю уявлення про те, як його можна було реалізувати. (Але зауважте, що це не завжди буде 0 рядків, а лише корисний тестер, який легко забирає такі гонки, як ця, і деякі складніші гонки. Дивіться коментар @Gilles .) Це не гарантувало б безпеку сценарію , але може бути корисним інструментом для тестування, подібним до тестування багатопотокової програми на різних процесорах, включаючи слабко упорядковані процесори без x86, як ARM.

Ви б запустили його як racechecker bash foo.sh

Використовуйте той же системний виклик трасування / перехоплення об'єктів , які strace -fі ltrace -fвикористовувати для кріплення до кожного дочірньому процесу. (У Linux це той самий ptraceсистемний виклик, який використовується GDB та іншими відладчиками для встановлення точок прориву, одного кроку та зміни пам’яті / регістрів іншого процесу.)

Інструмент openі openatсистемні виклики: коли будь-який процес , що працює в рамках цього інструменту робить системний виклик (або ) з , сном, може бути , 1/2 або 1 секунду. Нехай інші системні дзвінки (особливо ті, в тому числі ) виконують без зволікань.open(2)openatO_RDONLYopenO_TRUNC

Це повинно дати змогу письменникові виграти гонку майже в будь-яких умовах гонки, якщо тільки завантаження системи також не було високим, або це був складний стан гонки, коли врізання відбулося не після того, як прочитали інше. Тож випадкові зміни, які затримуються open()s (а може бути read(), і з записом), збільшать потужність виявлення цього інструменту, але, звичайно, без тестування на нескінченну кількість часу з тренажером затримки, який врешті-решт охопить усі можливі ситуації, з якими ви можете зіткнутися у реальному світі, ви не можете бути впевнені, що ваші сценарії не містять перегонів, якщо ви не прочитаєте їх уважно і не докажете, що їх немає.


Ймовірно, вам знадобиться його в білий список (не затягувати open) для файлів у, /usr/binі /usr/libтому процес запуску не займе вічно. (Динамічне підключення під час виконання має open()декілька файлів (дивіться strace -eopen /bin/trueчи /bin/lsколись), хоча якщо сама батьківська оболонка робить усічення, це буде нормально. Але цей інструмент все одно буде добре робити сценарії нерозумно повільно).

Або, можливо, у списку кожного файлу процес виклику в першу чергу не має дозволу на скорочення. тобто процес відстеження може здійснити access(2)системний виклик, перш ніж фактично призупинити процес, який хотів до open()файла.


racecheckerСам повинен був би бути написаний на С, а не в оболонці, але, можливо, він може використовувати straceкод як вихідний пункт і не потребує багато роботи для реалізації.

Можливо, ви можете отримати таку ж функціональність з файловою системою FUSE . Напевно, є FUSE-приклад чистої прохідної файлової системи, тож ви можете додати перевірки до open()функції в тій, що робить її сном для відкриття лише для читання, але нехай зрізання відбувається негайно.


Ваша ідея гоночної перевірки насправді не працює. По-перше, проблема в тому, що тайм-аути не є надійними: одного дня інший хлопець займе більше часу, ніж ви очікували (це класична проблема зі складанням або тестовими сценаріями, які, здається, працюють деякий час, а потім виходять з ладу важкими способами налагодження коли навантаження збільшується і багато речей паралельно працюють). Але поза цим, до якого відкритого ви збираєтесь додати затримку? Для того, щоб виявити щось цікаве, вам потрібно зробити багато запусків з різними моделями затримки та порівняти їх результати.
Жил "ТАК - перестань бути злим"

@Gilles: Правильно, будь-яка розумно-коротка затримка не гарантує, що усікання виграє гонку (на сильно завантаженій машині, як ви вказуєте). Ідея тут полягає в тому, що ви використовуєте це для тестування свого сценарію кілька разів, а не того, що ви використовуєте racecheckerвесь час. І, напевно, ви хочете, щоб час сну, який читається, був налаштований на користь людей на дуже сильно завантажених машинах, які хочуть встановити його вище, як 10 секунд. Або встановіть його нижче, як 0,1 секунди для тривалого або неефективні сценарії , які повторно відкрити файли лота .
Пітер Кордес

@Gilles: Чудова ідея про різні схеми затримки, які можуть давати вам можливість скористатися більшою частиною перегонів, ніж просто прості речі в тій самій конвеєрі, які "повинні бути очевидні (як тільки ви дізнаєтесь, як працюють снаряди)", як у випадку з ОП. Але "що відкривається?" будь-який відкритий доступ лише для читання, з білим списком або іншим способом не затримувати запуск процесу.
Пітер Кордес

Я думаю, ви думаєте про більш складні гонки з фоновими завданнями, які не скорочуються, поки не завершиться якийсь інший процес? Так, для того, щоб знайти це, може знадобитися випадкова варіація. Або, можливо, подивіться на дерево процесів і затримайте "раннє" читання більше, щоб спробувати перевернути звичайне замовлення. Ви можете зробити цей інструмент все складнішим для імітації нових і більше можливостей переупорядкування, але в якийсь момент вам все-таки доведеться правильно розробити свої програми, якщо ви робите багатозадачні завдання. Автоматизоване тестування може бути корисним для більш простих сценаріїв, де можливі проблеми обмежені.
Пітер Кордес

Це досить схоже на тестування багатопотокового коду, особливо без замкнених алгоритмів: логічні міркування про те, чому це правильно, дуже важливо, а також тестування, тому що ви не можете розраховувати на тестування на будь-якому конкретному наборі машин, щоб створити всі переупорядкування, які можуть бути бути проблемою, якщо ви не закрили всі лазівки. Але так само, як тестування на слабко упорядкованій архітектурі, як ARM або PowerPC, є хорошою ідеєю на практиці, тестування сценарію в системі, яка штучно затримує речі, може викрити деякі перегони, тому це краще, ніж нічого. Ви завжди можете вводити помилок, які він не застане!
Пітер Кордес

18

Чому існує умова гонки

Дві сторони труби виконані паралельно, не одна за одною. Існує дуже простий спосіб продемонструвати це: запустити

time sleep 1 | sleep 1

Це займає одну секунду, а не дві.

Оболонка запускає два дочірні процеси і чекає, поки вони завершаться. Ці два процеси виконуються паралельно: єдина причина, чому один з них синхронізується з другим, це коли йому потрібно чекати іншого. Найпоширеніша точка синхронізації - це коли права частина блокує очікування для зчитування даних на її стандартному вході і стає розблокованою, коли ліва сторона записує більше даних. Зворотне може трапитися і тоді, коли права рука повільно читає дані, а ліва блокує в процесі запису, поки права частина не зчитує більше даних (в самій трубі є буфер, яким керує ядро, але воно має невеликий максимальний розмір).

Щоб спостерігати за точкою синхронізації, дотримуйтесь наступних команд ( sh -xдрукує кожну команду під час її виконання):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Грайте з варіаціями, поки вам не сподобається те, що ви спостерігаєте.

Дано складену команду

cat tmp | head -1 > tmp

лівий процес виконує наступні дії (я перераховував лише кроки, які стосуються мого пояснення):

  1. Виконати зовнішню програму catз аргументом tmp.
  2. Відкрити tmpдля читання.
  3. Поки він не дійшов до кінця файлу, прочитайте фрагмент з файлу та запишіть його на стандартний вихід.

Правий процес робить наступне:

  1. Перенаправити стандартний висновок на tmp, обрізання файлу в процесі.
  2. Виконати зовнішню програму headз аргументом -1.
  3. Прочитайте один рядок зі стандартного вводу та запишіть його на стандартний вихід.

Єдиний момент синхронізації полягає в тому, що правий-3 чекає, поки лівий-3 обробить один повний рядок. Немає синхронізації між лівою-2 та правою-1, тому вони можуть відбуватися в будь-якому порядку. Який порядок вони відбуваються, не передбачувано: це залежить від архітектури процесора, оболонки, ядра, від того, які ядра мають бути заплановані, від того, що перериває процесор, отриманий за цей час тощо.

Як змінити поведінку

Ви не можете змінити поведінку, змінивши системне налаштування. Комп’ютер робить те, що ви йому наказали робити. Ви сказали йому, щоб усікати tmpі читати з tmpпаралельно, так це робить дві речі паралельно.

Гаразд, є одне «системне налаштування», яке ви можете змінити: ви можете замінити /bin/bashіншою програмою, яка не є баш. Я сподіваюся, що це само собою зрозуміло, що це не дуже гарна ідея.

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

{ cat tmp | head -1; } >tmp

або

( exec >tmp; cat tmp | head -1 )

Я не маю поняття, чому ви хочете цього хотіти. Який сенс читати з файлу, який ви знаєте порожнім?

І навпаки, якщо ви хочете, щоб перенаправлення виводу (включаючи усікання) відбулося після catзакінчення читання, вам потрібно або повністю забуферувати дані в пам'яті, наприклад

line=$(cat tmp | head -1)
printf %s "$line" >tmp

або запишіть у інший файл, а потім перемістіть його на місце. Зазвичай це надійний спосіб робити речі за сценаріями і має перевагу в тому, що файл пишеться повністю, перш ніж він буде видно через оригінальне ім'я.

cat tmp | head -1 >new && mv new tmp

Колекція moreutils включає в себе програму, яка робить саме це, що називається sponge.

cat tmp | head -1 | sponge tmp

Як визначити проблему автоматично

Якщо вашою метою було взяти неправильно написані сценарії та автоматично визначити, де вони ламаються, то, вибачте, життя не так просто. Аналіз виконання не може надійно знайти проблему, оскільки іноді catзакінчується читання, перш ніж відбудеться усічення. Статичний аналіз в принципі може це зробити; спрощений приклад у вашому запитанні потрапляє в Shellcheck , але він може не сприймати подібну проблему в більш складному сценарії.


Це було моєю метою визначити, добре написаний сценарій чи ні. Якщо сценарій, можливо, знищив дані таким чином, я просто хотів, щоб вони кожного разу їх знищували. Не добре чути, що це майже неможливо. Завдяки вам, я тепер знаю, у чому проблема, і спробую придумати рішення.
karlosss

@karlosss: Хм, мені цікаво, чи можна було б використовувати той самий матеріал відстеження / перехоплення системних викликів, як strace(наприклад, Linux ptrace), щоб змусити всі- openдля читання системних дзвінків (у всіх дочірніх процесах) спати на півсекунди, тому під час перегонів з усічення, усічення майже завжди виграє.
Пітер Кордес

@PeterCordes Я новачок у цьому, якщо ти зможеш керувати способом цього і записати його як відповідь, я прийму це.
karlosss

@PeterCordes Ви не можете гарантувати, що усічення виграє із запізненням. Він працюватиме більшу частину часу, але час від часу на сильно завантаженій машині ваш сценарій буде виходити з ладу більш-менш таємничими способами.
Жил "ТАК - перестань бути злим"

@Gilles: Давайте обговоримо це під моєю відповіддю.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.