Ограничение стандартного ввода-вывода C и почему мы не можем использовать стандартный ввод-вывод C с сокетами

Я читаю CSAPP в последнее время. В разделе 10.9 говорится, что стандартный ввод-вывод не следует использовать с сокетом по следующим причинам:

(1) Ограничения стандартного ввода/вывода

Ограничение 1: Функции ввода следуют за функциями вывода. Функция ввода не может следовать за функцией вывода без промежуточного вызова fflush, fseek, fsetpos или перемотки назад. Функция fflush очищает буфер, связанный с потоком. Последние три функции используют функцию Unix I/O lseek для сброса текущей позиции в файле.

Ограничение 2: Функции вывода следуют за функциями ввода. Выходная функция не может следовать за входной функцией без промежуточного вызова fseek, fsetpos или перемотки, если входная функция не встречает конец файла.

(2) Использование функции lseek для сокета является незаконным.

Вопрос 1. Что произойдет, если я нарушу ограничение? Я написал фрагмент кода, и он отлично работает.

Вопрос 2. Чтобы обойти ограничение 2, можно использовать следующий подход:

File *fpin, *fpout;

fpin = fdopen(sockfd, "r");
fpout = fdopen(sockfd, "w");

/* Some Work Here */

fclose(fpin);
fclose(fpout);

В учебнике было сказано,

Закрытие уже закрытого дескриптора в многопоточной программе — прямой путь к катастрофе.

Почему?


person feirainy    schedule 16.12.2013    source источник


Ответы (1)


Ваш обходной путь не работает так, как написано, из-за упомянутой вами ошибки двойного закрытия. Двойное закрытие безвредно в однопоточных программах, пока нет промежуточных операций, которые могут открыть новые файловые дескрипторы (второе закрытие просто безвредно завершится с ошибкой EBADF), но они являются критическими ошибками в многопоточных программах. Рассмотрим этот сценарий:

  • Поток A вызывает close(n).
  • Поток B вызывает open и возвращает n, который сохраняется как int fd1.
  • Поток A снова вызывает close(n).
  • Поток B снова вызывает open и снова возвращает n, который сохраняется как fd2.
  • Поток B теперь пытается записать в fd1 и фактически записывает в файл, открытый вторым вызовом open, а не в тот, который был открыт первым.

Это может привести к массовому повреждению файлов, утечке информации (представьте, что вы записываете пароль в сокет, а не в локальный файл) и т. д.

Однако проблему легко исправить. Вместо того, чтобы дважды вызывать fdopen с одним и тем же файловым дескриптором, просто используйте dup, чтобы скопировать его и передать копию fdopen. С этим простым исправлением stdio прекрасно работает с сокетами. Он по-прежнему не подходит для использования в асинхронном цикле событий, но если вы используете потоки для ввода-вывода, он прекрасно работает.

Редактировать: кажется, я пропустил ответ на ваш вопрос 1. Что произойдет, если вы нарушите правила о том, как переключаться между вводом и выводом в потоке stdio, это неопределенное поведение. Это означает, что тестировать его и видеть, что он «работает», не имеет смысла; это может означать либо:

  1. Реализация C, которую вы используете, предоставляет определение (как часть документации) того, что происходит в этом случае, и соответствует тому поведению, которое вы хотели. В этом случае вы можете использовать его, но ваш код не будет переносим на другие реализации. По этой причине это считается очень плохой практикой. Или же,

  2. Вы просто случайно получили ожидаемый результат, обычно как побочный эффект того, как соответствующая функциональность реализована внутри используемой вами реализации. В этом случае нет гарантии, что у него нет крайних случаев, которые не работают так, как вы ожидали, или что он продолжит работать так же в будущих выпусках и т. д.

person R.. GitHub STOP HELPING ICE    schedule 16.12.2013
comment
Спасибо за подробное объяснение. Но я не совсем понимаю использование асинхронного цикла событий. - person feirainy; 16.12.2013