Почему cat x >> x зацикливается?

17

Следующие команды bash входят в цикл infinte:

$ echo hi > x
$ cat x >> x

Я могу догадаться, что catпродолжает читать xпосле того, как он начал писать на стандартный вывод. Что сбивает с толку, так это то, что моя собственная тестовая реализация cat демонстрирует другое поведение:

// 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циклы, а вышеприведенный код нет?

Тайлер
источник
Это делает петлю для меня. Вы пытались запустить его под strace / truss? На какой ты системе?
Стефан Шазелас
Кажется, что BSD cat имеет такое поведение, и GNU cat сообщает об ошибке, когда мы пытаемся что-то вроде этого. В этом ответе обсуждается то же самое, и я полагаю, что вы используете BSD cat, так как у меня GNU cat и при тестировании я получил ошибку.
Рамеш
Я использую Дарвина. Мне нравится идея, которая cat x >> xвызывает ошибку; однако, эта команда предлагается в книге Unix Кернигана и Пайка в качестве упражнения.
Тайлер
3
catскорее всего использует системные вызовы вместо stdio. С помощью stdio ваша программа может кэшировать EOFness. Если вы начинаете с файла размером более 4096 байт, получается ли бесконечный цикл?
Марк Плотник
@MarkPlotnick, да! Код C зацикливается, когда размер файла превышает 4 КБ. Спасибо, возможно, в этом вся разница.
Тайлер

Ответы:

12

В более старой системе RHEL, которая у меня есть, цикл не/bin/cat выполняется . выдает сообщение об ошибке «cat: x: input file is output file». Я могу обмануть , делая это . Когда я пробую ваш код выше, я получаю "зацикливание", которое вы описываете. Я также написал системный вызов на основе «кота»:cat x >> xcat/bin/catcat < 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, например, для правильной работы - вы не можете позволить себе предположить, что позиция после окончания вашей последней записи по-прежнему остается концом файла.
Хмакхольм покинул Монику
1

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

Для других реализаций это зависит от того, больше ли файл, чем буфер ввода-вывода.

Шили
источник
Многие catреализации не буферизуют свой вывод ( -uподразумевается). Те всегда будут петлей.
Стефан Шазелас
Похоже, что Solaris 11 (SunOS-5.11) не использует mmap () для небольших файлов (кажется, что прибегает к нему только для файлов размером 32769 байт или более).
Стефан Шазелас
Правильный -u обычно используется по умолчанию. Это не подразумевает цикл, так как реализация может прочитать весь размер файла и сделать только одну запись с этим buf.
Шили
Solaris cat зацикливается только в том случае, если размер файла> максимальный размер карты или если начальное смещение файла равно! = 0.
schily
Что я наблюдаю в Solaris 11. Он выполняет цикл read (), если начальное смещение равно! = 0 или размер файла между 0 и 32768. Кроме того, он mmaps () 8MiB больших областей файла за раз и никогда кажется, возвращаются к циклам read () даже для файлов PiB (проверено на разреженных файлах).
Стефан Шазелас
0

Как написано в подводных камнях Bash , вы не можете читать из файла и записывать в него в том же конвейере.

В зависимости от того, что делает ваш конвейер, файл может быть закрыт (до 0 байтов или, возможно, до количества байтов, равного размеру буфера конвейера вашей операционной системы), или он может увеличиваться, пока не заполнит доступное дисковое пространство или не достигнет ограничение размера файла вашей операционной системы или квота и т. д.

Решением является либо использование текстового редактора, либо временная переменная.

MatthewRock
источник
-1

У вас есть какое-то состояние гонки между ними x. Некоторые реализации cat(например, coreutils 8.23) запрещают следующее:

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

Если это не обнаружено, поведение, очевидно, будет зависеть от реализации (размер буфера и т. Д.).

В вашем коде вы можете попытаться добавить a clearerr(f);после fflush, если next freadвернет ошибку, если установлен индикатор конца файла.

vinc17
источник
Кажется, что хорошая ОС будет иметь детерминированное поведение для одного процесса с одним потоком, выполняющим те же команды чтения / записи. В любом случае, поведение для меня детерминированное, и я в основном спрашиваю о несоответствии.
Тайлер
@ Tyler IMHO, без четкой спецификации в этом случае, приведенная выше команда не имеет смысла, и детерминизм не очень важен (за исключением такой ошибки, как здесь, которая является лучшим поведением). Это немного похоже на i = i++;неопределенное поведение C , отсюда и расхождение.
vinc17
1
Нет, здесь нет расы, поведение четко определено. Однако он определяется реализацией, в зависимости от относительного размера файла и используемого буфера cat.
Жиль "ТАК - перестань быть злым"
@ Жиль Где вы видите, что поведение четко определено / определено реализацией? Можете ли вы дать ссылку? Спецификация POSIX cat просто говорит: «Это определяется реализацией, буферизует ли утилита cat, если не указана опция -u». Однако, когда используется буфер, реализация не должна определять, как он используется; это может быть недетерминированным, например, с буфером, очищенным в случайное время.
vinc17
@ vinc17 Пожалуйста, добавьте «на практике» в мой предыдущий комментарий. Да, это теоретически возможно и POSIX-совместимо, но никто этого не делает.
Жиль "ТАК - перестань быть злым"