Для следующего кода:
#include <unistd.h>
#include <stdio.h>
int main()
{
char buf[6];
printf("%c\n", (char)getc(stdin));
int ret = read(STDIN_FILENO, buf, 5);
printf("%d\n", ret);
printf("%c\n", (char)getc(stdin));
}
При использовании «abcdef» в качестве входных данных система выводит
a
0
b
Я думаю, это означает, что в данном случае read ничего не может получить. Однако при закомментировании первого вызова getc система выводит
5
f
Я думаю, это означает, что read
можно что-то получить.
Мой вопрос: как избежать блокировки read
от getc
?
Кстати: вы можете воспроизвести поведение из https://godbolt.org/z/EaPTMWPz9
Единственный способ предотвратить блокировку getc
при чтении — либо предоставить данные в канале, либо закрыть канал. (Или вызвать ошибку чтения, или пометить fd как неблокирующий). Но я не думаю, что это действительно вопрос. Поведение, которое вы видите, ожидаемо и разумно; Что именно ты пытаешься сделать? Есть ли что-то в наблюдаемом поведении, что сбивает с толку?
Я понял, что они не означают «заблокировано», как «блокировка ввода-вывода», меня это тоже смутило. Они просто спрашивают, почему read
не получает ожидаемых данных.
Не проблема, но read
возвращает ssize_t
, а не int
. (При переключении вам также придется переключить спецификатор формата на %zd
.)
getc
ничего не блокирует. getc
— буферизованный ввод. Сначала он считывает всю строку из STDIN_FILENO
в буфер, связанный с stdin
, а затем просто возвращает один символ из буфера каждый раз, когда вы его вызываете, пока он не станет пустым.
read
— это функция POSIX, а не стандартная часть языка C, которая работает с базовыми дескрипторами файлов. Это непереносная деталь платформы. В этом случае он подключен через STDIN_FILENO
ко входу (например, каналу, если вы запустили echo "abcdef" | ./a.out
), в котором ничего не осталось, потому что getc
уже прочитал все во внутренний буфер stdin
.
Или
fread(..., stdin)
с fgetc(stdin)
, и позвольте ему управлять буферизацией за вас, ИЛИТолько не смешивайте их для одного и того же потока, все происходит именно так, как вы наблюдали.
Примечание. Причина комментария о блокировке заключается в том, что блокирующий и неблокирующий ввод-вывод — это особая тема, которая совершенно выходит за рамки данной статьи, но формулировки достаточно схожи, чтобы немного сбивать с толку.
Что вы подразумеваете под словами «В данном случае он подключен к трубе…»? Вы имеете в виду конкретно, что godbolt осуществляет ввод через канал? Или вообще stdin реализован через канал?
По какой-то причине я предполагал, что OP работает echo "abcdef" | ./a.out
или что-то подобное, но не могу вспомнить почему. Уточню.
Согласно POSIX 2.5.1 Взаимодействие файловых дескрипторов и стандартных потоков ввода-вывода:
В этом разделе описывается взаимодействие файловых дескрипторов и стандартных потоков ввода-вывода. Функциональность, описанная в этом разделе, является расширением стандарта ISO C...
...
Результат вызовов функций, включающих любой один дескриптор («активный дескриптор»), определен в другом месте этого тома POSIX.1-2017, но если используются два или более дескрипторов, и любой из них является потоком, приложение должно убедитесь, что их действия скоординированы, как описано ниже. Если этого не сделать, результат не определен.
После этого существует длинный список правил, которым необходимо следовать, чтобы не вызвать неопределенное поведение.
Ни одно из этих правил не предполагает чтение из одного и того же источника через дескриптор файла и поток FILE *
.
Таким образом, неопределенное поведение вызывается, когда вы пытаетесь прочитать из того же базового источника через [p]read()
в файловом дескрипторе, а также прочитать из того же источника через поток FILE *
, например getc(stdin)
.
Я понимаю концепцию, лежащую в основе: «Это неопределенное поведение, поэтому не тратьте время на размышления, почему вы видите то, что видите — с такой же легкостью это может привести к тому, что единорог выпрыгнет из вашего экрана». Но объяснения в других ответах дают полезную информацию о том, как работают эти слои. Я думаю, что лучше всего сочетать эти два варианта: рассказать, что на самом деле происходит, а затем сказать, что не следует рассчитывать на такое поведение, поскольку оно не определено.
@StreveFord Не было смысла повторять другой ответ(ы). Понимание неопределенного поведения может быть интересным, но практически невозможно применить это понимание каким-либо надежным способом. А если оно ненадежно, то это хуже, чем бесполезно – это опасно. Тот факт, что единорог не выпрыгивает из вашего экрана, не означает, что вы имеете какой-либо контроль над тем, что происходит. If может работать нормально, и без изменений произойдет сбой при следующем запуске. Это ужасно низкий стандарт для написания кода.
Это всего лишь вопрос о буферизации. Первый getc
считывает из файла намного больше одного байта. Обратите внимание, что слово «файл» здесь неоднозначно, поскольку существует несколько уровней абстракции, но я не хочу беспокоиться о деталях.
По сути, чтение из файла происходит медленно, и система ускоряет процесс, выполняя медленные операции как можно реже. Когда вы вызываете getc
в первый раз, система считывает кучу данных из базового потока, что-то вроде 4096 или 8192 байт, или какой-то разумный размер для платформы. Вызов read
после getc
возвращает 0, поскольку система уже прочитала все данные. Следующий getc
возвращает данные из буфера, записанного первым getc
.
Если вы вызовете read
до вызова getc
, данные не будут буферизованы и все еще доступны для чтения.
По сути, вызов read
пытается обойти буферизацию, которую выполняет getc
.
Мне нравится ваш ответ, потому что он объясняет, почему выполняется буферизованный ввод-вывод. Но вы конкретно не говорите, что вы не должны смешивать стандартный буферизованный ввод-вывод библиотеки c с вводом-выводом POSIX (чтение/запись). Ты должен это сказать. Кроме того, было бы полезно упомянуть, что, хотя стандартный ввод-вывод библиотеки C можно найти на многих платформах, отличных от Unix (например, Windows), POSIX не так распространен в операционных системах, отличных от Unix (например, Windows).
@StreveFord обычно ответы стараются не дублировать. Если в одном ответе уже говорилось об отказе от смешивания ввода-вывода, обычно не требуется дублировать эту информацию в каждом ответе. В противном случае все ответы на SO были бы просто копиями всех предыдущих ответов плюс какой-то новой мыслью.
Что ж, это одна из причин, по которой не следует смешивать методы ввода/вывода из/в один и тот же поток. Есть ли для этого практическая причина?