Рассмотрим следующую программу на языке C, названную weird.c (поскольку то, что она делает, не имеет смысла):
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
off_t result = lseek(STDOUT_FILENO, (off_t)INT64_MAX, SEEK_SET);
if (result != (off_t)-1)
printf("Seek successful to position %" PRId64 "\n", (int64_t)INT64_MAX);
else
perror("lseek failed");
return 0;
}
Скомпилируйте и запустите его из окна терминала, например. gcc -o weird weird.c && ./weird. Важно, чтобы вы запускали его с терминала (окна терминала, сеанса SSH и т. д.), поэтому stdout — это терминал.
В Linux это завершится с ошибкой lseek failed: Illegal seek — что имеет смысл, поиск по tty не имеет особого смысла.
Однако в macOS это ведет себя по-другому — программа завершает работу, ничего не печатая, а затем — если вы используете bash в качестве оболочки, оболочка также завершает работу. (Напротив, если вы используете zsh в качестве оболочки, приглашение оболочки по-прежнему работает, но никакие команды не отображают никаких результатов.) Я могу только предположить, что это связано с тем, что tty был переведен в какое-то недопустимое состояние. Но что здесь происходит на самом деле?
Я предполагал, что macOS будет вести себя аналогично FreeBSD. Но FreeBSD 14.1-RELEASE (amd64) снова ведет себя по-другому: она печатает:
Seek successful to position 9223372036854775807
Это более понятно, чем то, что делает macOS (рассматривая бессмысленную операцию как недействующую, вместо того, чтобы выдавать ошибку, как это делает Linux).
Действительно странно! Я могу воспроизвести его (M3 MacBook Pro под управлением macOS Sonoma 14..61). После запуска программы echo $?, или ls, или ps не выдает никаких результатов — и lldb кажется зависающим. В итоге мне пришлось вручную закрыть окно. Измените смещение на 0 или 100000000, и (измененная) программа сообщит об успехе, не блокируя все действия. man lseek предусматривает, что ESPIPE («Незаконный поиск») будет возвращено, если дескриптор файла связан с каналом, сокетом или FIFO. Ни man -s 4 tty, ни man -s 4 pty не дают мне никакого освещения.
Кстати, если вы сломаете stdout, вы больше не увидите вывода printf(). Для отладки я бы записал такой вывод в файл и запустил на нем tail -f во втором терминале.
Чтобы прояснить поведение Linux, использование lseek на терминальном устройстве хорошо задокументировано здесь: «В Linux использование lseek() на терминальном устройстве возвращает ESPIPE».
Попробовал это на Win10 с MinGW 13.2.0, результат оказался таким же, как и ваш эксперимент с Linux.
В POSIX говорится: поведение lseek() на устройствах, которые не способны осуществлять поиск, определяется реализацией. Значение смещения файла, связанного с таким устройством, не определено.
Страница руководства MacOS для lseek выглядит так: Developer.apple.com/library/archive/documentation/System/… (хотя там написано iPhoneOS). Там говорится только: «Некоторые устройства не способны осуществлять поиск. Значение указателя, связанного с таким устройством, не определено». Я думаю, это можно понимать как указание на то, что поведение в MacOS не определено.
Поскольку MacOS является производной BSD, возможно, стоит подумать о том, что на странице руководства BSD говорится об этом (в разделе «Ошибки»): «Если системный вызов lseek() работает на устройстве, которое неспособно искать [другие чем канал, сокет или FIFO], он запросит операцию поиска и вернет ее успешно, даже если поиск не выполнялся. Поскольку аргумент смещения будет безусловно сохранен в файловом дескрипторе этого устройства, нет способа подтвердить, был ли он выполнен. операция поиска удалась или нет [...]».
@JohnBollinger Меня озадачила эта справочная страница MacOS. Может ли поведение, определяемое реализацией, быть неопределенным? Неприятные последствия для TTY кажутся системной ошибкой. Что, если это было сделано на системной консоли в однопользовательском режиме (если такое существует в MacOS)?
@IanAbbott, я не думаю, что реализация не может определить поведение, определенное реализацией, явно или неявно. Возможно, есть какое-то другое место, где MacOS определяет это конкретное поведение, но я думаю, что более вероятно, что оно просто не соответствует этому вопросу.
@IanAbbott Поскольку эмуляторы терминала и сеансы SSH используют ptys, а консоль использует какое-то реальное терминальное устройство, я подозреваю, что в этих двух случаях поведение совершенно разное.
Поведение Linux и Windows является ожидаемым результатом. Я согласен, что это ошибка в MacOS.
Это происходит только с INT64_MAX или с любым значением смещения?
@Barmar Возможно, вы правы насчет этих двух случаев, но в Linux файловые операции для всех типов TTY проходят через общий уровень подсистемы TTY.
@IanAbbott Поведение одинаково для всех tty, как в Linux, так и в macOS. В Linux: github.com/torvalds/linux/blob/v6.10/drivers/tty/tty_io.c#L465 жестких кодов llseek для всех tty к no_llseek, отключив поиск. Драйвер tty предоставляет struct tty_operations, членами которого являются функции для выполнения определенных операций, но поиск не входит в их число. И наоборот, EFBIG, когда write_offset == INT64_MAX в macOS 11+ происходит в коде VFS, поэтому применяется ко всем файлам/устройствам и никоим образом не зависит от tty.





Пользователь jepler на Hacker News указал на объяснение.
Обратите внимание на этот код в функции vn_write в bsd/vfs/vfs_vnops.c:
if (write_offset == INT64_MAX) {
/* writes are not possible */
error = EFBIG;
goto error_out;
}
В основном происходит следующее:
lseek для установки смещения терминала, хотя (обычно) смещение на самом деле ничего не делает.INT64_MAX, дальнейшие попытки записи завершатся неудачей с EFBIG, если только вы не вернетесь куда-то раньше.Причина выхода bash заключается в том, что большинство оболочек завершают работу, когда они не могут выполнить запись в stdout/stderr. Что касается того, почему zsh этого не делает, я не уверен, но предполагаю, что код редактирования строки zsh открывает терминальное устройство напрямую и, следовательно, обращается к нему с независимой позицией файла.
Если вы измените мою тестовую программу, чтобы, например, INT64_MAX-100, вы сможете напечатать 100 байт вывода, прежде чем весь вывод начнет давать сбой.
Этот код, похоже, был представлен в xnu-7195.50.7.100.1 (macOS 11 Big Sur) — его не было в xnu-6153.11.26 (macOS 10.15 Catalina).
FreeBSD также позволяет вам lseek выполнять произвольные смещения в терминале, но, в отличие от macOS 11+, не отказывает в дальнейших операциях записи, если вы ищете до INT64_MAX.
Я подозреваю, что это было задумано как проверка здравомыслия: «Здесь никогда не должно быть». Не похоже, что есть веская причина искать так далеко в терминале (или в любом реальном файле).
Почему вы там выводите
INT64_MAX?