Чому цикл x >> x?


17

Наступні команди bash переходять у нескінченний цикл:

$ echo hi > x
$ cat x >> x

Я можу здогадатися, що catпродовжує читати з xтого часу, коли він почав писати до stdout. Що бентежить, це те, що моя власна тестова реалізація кота проявляє різну поведінку:

// mycat.c
#include <stdio.h>

int main(int argc, char **argv) {
  FILE *f = fopen(argv[1], "rb");
  char buf[4096];
  int num_read;
  while ((num_read = fread(buf, 1, 4096, f))) {
    fwrite(buf, 1, num_read, stdout);
    fflush(stdout);
  }

  return 0;
}

Якщо я біжу:

$ make mycat
$ echo hi > x
$ ./mycat x >> x

Це не циклічно. Враховуючи поведінку catта те, до чого я звертаюся до stdoutтого fread, як я знову закликаюсь, я б очікував, що цей код C продовжить читання та запис у циклі.

Наскільки ці дві поведінки послідовні? Який механізм пояснює, чому catциклі, тоді як наведений вище код не робить?


Це робить цикл для мене. Ви спробували запустити його під напругою / фермою? На якій системі ви працюєте?
Stéphane Chazelas

Здається, BSD кішка має таку поведінку, і GNU cat повідомляє про помилку, коли ми намагаємось щось подібне. Ця відповідь обговорює те саме, і я вважаю, що ви використовуєте BSD cat, оскільки я маю GNU cat і коли перевіряли помилку.
Рамеш

Я використовую Дарвіна. Мені подобається ідея, яка cat x >> xвикликає помилку; однак ця команда запропонована в книзі Unix Керніган та Пайк як вправа.
Тайлер

3
catшвидше за все, використовує системні виклики замість stdio. За допомогою stdio ваша програма може кешувати EOFness. Якщо ви почнете з файлу розміром більше 4096 байт, ви отримуєте нескінченний цикл?
Марк Плотнік

@MarkPlotnick, так! Код С циклікує, коли файл перевищує 4 к. Дякую, можливо, у цьому вся різниця.
Тайлер

Відповіді:


12

На старій системі RHEL я отримав, /bin/catробить НЕ петля для cat x >> x. catвидає повідомлення про помилку "cat: x: вхідний файл - вихідний файл". Я можу обдурити /bin/cat, роблячи це: cat < x >> x. Коли я спробую ваш код вище, я отримую опис, який ви описуєте. Я також написав "кіт" на основі системного дзвінка:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int
main(int ac, char **av)
{
        char buf[4906];
        int fd, cc;
        fd = open(av[1], O_RDONLY);
        while ((cc = read(fd, buf, sizeof(buf))) > 0)
                if (cc > 0) write(1, buf, cc);
        close(fd);
        return 0;
}

Ця петля теж. Єдине буферування тут (на відміну від "mycat" на основі stdio) - це те, що відбувається в ядрі.

Я думаю , що це відбувається, що дескриптор файл 3 (результат open(av[1])) має зміщення в файл 0. подати дескриптор 1 (STDOUT) має зміщення 3, тому що «>>» призводить до того , що посилається оболонці , щоб виконати команду lseek()на дескриптор файлу, перш ніж передавати його catдочірньому процесу.

Рухається read()будь-якого роду, будь то в STDIO буфер, або циліндричне char buf[]досягнення положення дескриптора файлу 3. Ведення write()переміщує позицію дескриптора файлу 1. Ці два зсуву різних чисел. Через ">>" дескриптор 1 файлу завжди має зсув, більший або рівний зміщенню дескриптора файлу 3. Отже, будь-яка програма "схожа на кішку" буде циклічною, якщо вона не виконує внутрішню буферизацію. Можливо, можливо, навіть ймовірно, що stdio-реалізація FILE *(що є типом символів stdoutта fу вашому коді), що включає власний буфер. fread()може фактично зробити системний виклик read()для заповнення внутрішнього буфера fo f. Це може або не може нічого змінити у внутрішніх сторонах stdout. виклик fwrite()наstdoutможе або не може нічого змінити всередині f. Отже, "кішка" на основі stdio може не зациклюватися. Або може. Важко сказати, не читаючи багато потворних, потворних кодів libc.

Я зробив straceна RHEL cat- він просто робить послідовність read()і write()системні дзвінки. Але catне потрібно працювати таким чином. Можна було б mmap()ввести файл, тоді зробіть write(1, mapped_address, input_file_size). Ядро зробило б всю роботу. Або ви могли б зробити sendfile()системний виклик між дескрипторами вхідного та вихідного файлів у системах Linux. За старими системами SunOS 4.x, по чутках, було зроблено трюк картографічної пам'яті, але я не знаю, чи хтось коли-небудь робив кота на основі sendfile. У будь-якому випадку , «зациклення» б не сталося, так як обидва write()і sendfile()вимагають параметра довжини до передачі.


Спасибі. У Дарвіні це виглядає як freadдзвінок, кешований прапором EOF, як запропонував Марк Плотнік. Докази: [1] Кіт Дарвіна використовує читання, а не фрейд; і [2] Фредон Дарвіна викликає __srefill, що встановлюється fp->_flags |= __SEOF;в деяких випадках. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Тайлер

1
Це приголомшливо - я вчора першим виголосив цю заяву. Це може бути варто відзначити , що тільки POSIX певний перемикач catє cat -u- у для небуферізованних .
mikeserv

Насправді, це >>повинно бути реалізовано за допомогою виклику open () з O_APPENDпрапором, що змушує кожну операцію запису (атомно) записувати до поточного кінця файлу, незалежно від того, яке положення дескриптора файлу було до читання. Така поведінка необхідна для foo >> logfile & bar >> logfileкоректної роботи, наприклад, ви не можете дозволити собі припустити, що позиція після закінчення вашого останнього запису все ще є кінцем файлу.
hmakholm залишився над Монікою

1

Сучасна реалізація котів (sunos-4.0 1988) використовує mmap () для відображення всього файлу, а потім викликає 1x write () для цього простору. Така реалізація не буде циклічною, поки віртуальна пам'ять дозволить зіставити весь файл.

Для інших реалізацій це залежить від того, чи більший файл буфера вводу / виводу.


Багато catреалізацій не захищають свій результат ( -uмається на увазі). Ці завжди будуть циклічно.
Стефан Шазелас

Solaris 11 (SunOS-5.11), схоже, не використовує mmap () для невеликих файлів (здається, вдаються до нього лише для файлів, розміром 32769 байтів чи вище).
Стефан Шазелас

Правильно -u зазвичай є типовим. Це не означає циклу, оскільки реалізація може прочитати цілий розмір файлів і виконати лише один запис із цим buf.
schily

Solaris cat циклічне, якщо розмір файлів> max mapize або якщо початковий зсув файлу! = 0.
schily

Що я спостерігаю з Solaris 11. Він робить цикл читання (), якщо початковий зсув! = 0 або якщо розмір файлів встановлений 0 і 32768. Вище це, він mmaps () 8MiB великих областей файлу одночасно і ніколи здається, повертаються до циклу читання () навіть для файлів PiB (тестовані на розріджених файлах).
Стефан Шазелас

0

Як написано в підводних каменах Баша , ви не можете прочитати з файлу і записати в нього тим же конвеєром.

Залежно від того, що робить ваш конвеєр, файл може бути клоберованим (до 0 байт або, можливо, на кількість байтів, рівним розміру буфера конвеєра вашої операційної системи), або він може зростати, поки він не заповнить доступний простір на диску або не досягне обмеження розміру файлу вашої операційної системи або квоти тощо

Рішення полягає у використанні текстового редактора або тимчасової змінної.


-1

У вас є певна умова гонки між обома x. Деякі реалізації cat(наприклад, coreutils 8.23) забороняють:

$ cat x >> x
cat: x: input file is output file

Якщо цього не виявити, поведінка, очевидно, буде залежати від реалізації (розмір буфера тощо).

У своєму коді ви можете спробувати додати clearerr(f);після fflush, у випадку, якщо наступний freadповерне помилку, якщо встановлено індикатор кінця файлу.


Здається, що хороша ОС матиме детерміновану поведінку для одного процесу з одним потоком, який виконує однакові команди читання / запису. У будь-якому випадку, поведінка для мене є детермінованою, і я в основному запитую про невідповідність.
Тайлер

@ Tyler IMHO, без чітких конкретизацій щодо цього випадку, наведена вище команда не має сенсу, а детермінізм насправді не важливий (за винятком помилки, як тут, яка є найкращою поведінкою). Це трохи схоже на i = i++;невизначене поведінку С , звідси і розбіжність.
vinc17

1
Ні, тут немає перегонів, поведінка чітко визначена. Однак це визначається реалізацією, залежно від відносного розміру файлу та буфера, який використовується cat.
Жил 'ТАК - перестань бути злим'

@Gilles Де ви бачите, що поведінка чітко визначена / визначена реалізацією? Ви можете дати посилання? Специфікація коду POSIX просто говорить: "Це визначається реалізацією, чи буферизація утиліти cat виводить, якщо параметр -u не вказаний." Однак, коли використовується буфер, реалізація не повинна визначати спосіб її використання; це може бути недетерміновано, наприклад, з буфером, який промивається у випадковий час.
vinc17

@ vinc17 Будь ласка, введіть "на практиці" у мій попередній коментар. Так, це теоретично можливо і сумісно з POSIX, але ніхто цього не робить.
Жил 'ТАК - перестань бути злим'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.