Страницы

Поиск по вопросам

Показаны сообщения с ярлыком буферизация. Показать все сообщения
Показаны сообщения с ярлыком буферизация. Показать все сообщения

среда, 22 января 2020 г.

Буферизация stdout, работа fflush

#cpp #linux #c #syscall #буферизация


Что делает fflush? Многие пишут,что эта функция дает команду ОС сбросить содержимое
буфера на диск. Но разве этим занимается ОС? Насколько я понял при работе с файлами
буфер создает сама программа, например, функция fopen. В таблице syscall-ов linux я
не нашел чего-то похожего на flush. При этом в пример приводят вот такой код:

#include 
int main() {
  printf("Hello");
  while(true);
  return 0;
}


При запуске такой программы, hello напечатано не будет, если не добавить после printf
вызов fflush(stdout). Однако вот такой код вполне нормально работает:

#include 
#include 
int main() {
  char b[] = "Hello";
  write(1,(void*)b,sizeof(b)-1);
  while(true);
  return 0;
}


Тут мы не используем высокоуровневые методы работы с stdout, а работаем на уровне
syscall-ов. Я правильно понимаю что буферизация работы с файлами реализована средствами
самой программы, а не ОС? И при вызове write, данные сразу будут записаны на диск?
    


Ответы

Ответ 1



Что касается fflush, в сущности всё так и есть. Семейство функций для работы с потока в Си (fopen/fread/fprintf итп) fflush сбрасывает только оные буферы. Они же могут быть сброшены и раньше по исчерпанию (размер стандартного буфера BUFSIZ [8к для glibc/linux/x86]) или в зависимости от настроек буферизации (см. man 3 setbuf), в частности по-умолчанию для обычных файлов используется блочная буферизация (сбрасывается по 4к для glibc/linux), для терминалов (втч для stdout — строчная, сбрасывается по символу '\n'), для stderr буферизации нет. Для Linux собственно во время сброса буфера происходит системный вызов write(), а что происходит с данными после сброса этого стандарт Си не гарантирует. Из man 3 fflush: ЗАМЕЧАНИЯ Заметим, что fflush() сбрасывает буферы только пользовательского пространства, заданные библиотекой Си. Чтобы гарантировать, что данные действительно физически сохранены на диске, буферы ядра также должны быть сохранены, например, с помощью вызова sync(2) или fsync(2). Что происходит с данными после write() зависит от того с каким файлом связан дескриптор и как он был открыт. Практически в любом случае ОС практически всегда проводит дополнительную буферизацию/кеширование. Очевидное исключение, например, запись в /dev/null; в принципе возможно существование устройств не буферизирующих ничего, а работающих напрямую с буфером из юзерспейса, но на вскидку я таких не назову Не вдаваясь в детали в Linux'е для обычного файла на обычной дисковой ФС при не-синхронном вводе-выводе происходит следующее: ядро вносит изменения в кеше страниц и помечает их как грязные, затем возвращает управление процессу. Далее специальный демон (раньше был один pdflush (с несколькими потоками), ныне для каждого диска/блочного устройства/некоторых ФС свой) следит за грязными страницами и отдаёт команды драйверам ФС сохранить данные на диск при одном из условий: Когда не хватает свободной памяти Когда грязных страниц слишком много (см. /proc/sys/vm/dirty_{ratio,background_ratio}), По прошествии определённого времени (указанного в /proc/sys/vm/dirty_{writeback_centisecs,expire_seconds}) По вызову [f]sync() Когда ещё какая-то умная эвристика сработала. В этот момент ФС может преобразовать данные (например сжать или зашифровать). Далее создаётся запрос на вывод на блочное устройство. Когда драйвер жёсткого диска обработает этот запрос т.е. отправит данные непосредственно на устройство и получил подтверждение, что всё в порядке, страница снова отмечается, как чистая. При синхронном вводе-выводе (когда файл открыт с O_SYNC или ФС смонтирована с -o sync) всё точно также, но управление процессу из write() не будет возвращено пока страница не отмечена как чистая.

понедельник, 25 ноября 2019 г.

Как устроена работа буферизации в php?


Существует 2 вида буферов в php:


Cистемный буфер
Пользовательский буфер


Так могут возникнуть проблемы с работой с буфером.
Для работы с системным буфером есть одна единственная функция - flush(). Для работ
с пользовательским буфером остальные функции с приставкой ob_(Функция ob_implicit_flush() - не относится к данному списку, она больше подходит к системному буферу).

Код для демонстрации:


  Внимание использована инструкция в файле .htaccess
  php_value output_buffering off


";
for ($i = 1; $i <= 10; $i++) {
    echo $i.":(".mt_rand(0,1000).") ";
    flush();
    sleep(1);
}
echo "
LVL of buffer: ".ob_get_level()."
"; header("Hello"); session_start(); header("Support title"); echo "

Thx!

"; ?> Этим примером я хотел показать что при использовании только системного буфера м можем выводить кусочками сообщение на экран. Минусом является то что если захотим отправить заголовки или сессию то будет ошибка, т.к после отправки первого сообщения на экран всевозможные заголовки были отправлены и повторная передача заголовков невозможна. Код для демонстрации - 2: Внимание использована инструкция в файле .htaccess php_value output_buffering 4096 "; for ($i = 1; $i <= 10; $i++) { echo $i.":(".mt_rand(0,1000).") "; flush(); sleep(1); } echo "
LVL of buffer: ".ob_get_level()."
"; header("Hello"); session_start(); header("Support title"); echo "

Thx!

"; ?> В этом примере мы не увидим кусочный вывод текста мы увидем его только когда исполнитс весь код. Даже flush() здесь не поможет. Здесь между прочим используется не только системный но и пользовательский буфер. Кстати вылезут ошибочки по поводу заголовков...тут конечно я не могу объяснить ситуацию из-за чего. Я в конце выскажу свои предположения и вопросы...stay here. Код для демонстрации - 3: Внимание использована инструкция в файле .htaccess php_value output_buffering 4096 "; for ($i = 1; $i <= 10; $i++) { echo $i.":(".mt_rand(0,1000).") "; ob_flush(); flush(); sleep(1); } echo "
LVL of buffer: ".ob_get_level()."
"; header("Hello"); session_start(); header("Support title"); echo "

Thx!

"; ?> В этом примере информация будет выводиться кусками. Этот пример как вы могли заметит отличается только дополнительной функцией ob_flush(). Она таки высвобождает накопленны данные в системный буфер. Следом вызывается flush() который информацию выводит прямо в браузер. Первый пример кода тому явное подтверждение. В конце будут отправлены ошибки о заголовках, оно и понятно данные были уже отправлены. Код для демонстрации - 4: Внимание использована инструкция в файле .htaccess php_value output_buffering off "; for ($i = 1; $i <= 10; $i++) { echo $i.":(".mt_rand(0,1000).") "; sleep(1); } echo "
LVL of buffer: ".ob_get_level()."
"; header("Hello"); session_start(); header("Support title"); echo "

Thx!

"; ?> Интересно, а для чего я этот код добавил? Никаких флюшей здесь нет, даже функций которые работают с буфером вывода. Все гораздо проще чем может показаться на первы взгляд. Дело в том что если мы запустим этот код он отработает как надо и выведет ошибки об уже отправленных заголовках...what? Выходит системный буфер не такой и буфер если он вызывает ошибку о заголовках, однако данные выведутся только тогда когда закончится скрипт. Вот так вот. Эпилог: Мои суждения: Т.к существует 2 буфера в php то получается один зависит от другого: чтобы вывест информацию со свежевыпеченного echo, мы должны сначала высвободить информацию с "нашего" буфера в "казенный" php буфер, после этого мы должны вызвать операцию для "казенного" php буфера на вывод. Системный буфер нельзя удалить, а пользовательский можно. Информация всегда будет попадать в системный буфер хочешь ты того или нет! Пользовательский буфер - это станция где люди ожидают свой самолет. Системный буфе - это самолет, который вмещает в себя людей. Заголовки - это купленные билеты (Вы ж не можете купить билет уже в самолете (Ну по крайней мере я ни разу такого не видел)) если самолет имеет время ожидания вы можете купить билет прямо на станции. Одно без другого жить не может, нет пользовательского буфера - нет времени для покупки билета, нет системного буфера - нет самолета. Выводом буфера будет являться то, когда самолет прибудет на станцию и высадит пассажиров Вопросы: Мой второй пример, мне не понятно по какой причине мне в конце работы скрипта ph пишет ошибку, по факту flush() не срабатывает ввиду того что мы находимся в пользовательском буфере. (Хотя возможно то что в системном буфере ничего нет и этим вызовом мы уже отправляем заголовки) Если опять же во втором примере мы заменим flush() => ob_flush(), мы получим ошибк об отправленных заголовках...why? Как я писал есть 2 буфера системный и пользовательский по логике в системном буфере лежит пользовательский и как вы могли видеть при наличии пользовательского буфера необходимо вызвать 2 флюша, а если у нас имелся только системный то хватало системного флюша. Если мы в файле .htaccess напишем php_value output_buffering on то размер буфер станет unlimited как сообщает php.ini, отнюдь если мы в скрипте вызовем функцию ob_get_level( то получим 0, если вы запускали код, который я писал выше то могли заметить если у нас буфер был заданного размера то мы получали 1, если мы его удаляли или писали off то он у нас был 0 (хочу заметить я знаю что могут быть вложенные буферы и значение может быть куда выше)...собственно почему у нас 0 если размер буфера on? Переосмысление (ответы на первый, второй и третий вопросы): Если данные попадают в системный буфер то заголовки будут отправлены, не важно вызвали вы flush() или нет, как следствие - варн об от отправленных заголовках* Если вы вызовете flush(), но в системный буфер до этого ничего не попадало, это будет означать что данные нужно вывести на экран немедленно из системного буфера, как следствие - отправк заголовков и варн* Если вы пишите ob_flush() в пользовательском буфере с уровнем < 2 (это равносильно тому, что если вы пишите echo в системном буфере), то вы получите варн* так как вы пробрасываете содержимое в системный буфер, а как я писал выше системный буфер если видит какие-то выводимые данные он начинает отправлять заголовки, в этом случае происходит то же самое. Если мы в файле .htaccess напишем php_value output_buffering on при вызове phpinfo( в php файле мы увидим в колонке Local Value - on. В итоге у нас буфер 0! Как так? Вс просто. Эта директива (output_buffering) не поддерживает такое значение. У этой директив есть несколько состояний: 0 - выключенный буфер, 1 - безлимитный буфер и любое число. Кроме того если мы напишем текст вроде: on, off, true, false, sampletext - то в Local Value мы увидим подобные значения, но они будут интерпетироваться как 0. Если мы будем писать текст через пробел то получим ошибку 500, например php_value output_buffering hello world. * Если вы отправляете заголовки после. ** php_value, php_flag, output_buffering - директивы. P.S Возможно я что-то не правильно понял, не стоит воспринимать это как истину, любом случае я смог понять только так, по-другому объяснить свои вопросы не удалось. В любом случае я жду объяснения от людей, которые соображают


Ответы

Ответ 1



Отвечу только на часть вашего вопроса, по крайней мере на текущий момент. Ваш код: "; for ($i = 1; $i <= 10; $i++) { echo $i.":(".mt_rand(0,1000).") "; sleep(1); } echo "
LVL of buffer: ".ob_get_level()."
"; header("Hello"); session_start(); header("Support title"); echo "

Thx!

"; ?> Ваш комментарий к нему: Интересно, а для чего я этот код добавил? Никаких флюшей здесь нет, даже функций, которые работают с буфером вывода. Все гораздо проще чем может показаться на первый взгляд. Дело в том что если мы запустим этот код он отработает как надо и выведет ошибки об уже отправленных заголовках...what? Выходит системный буфер не такой и буфер если он вызывает ошибку о заголовках, однако данные выведутся только тогда когда закончится скрипт. Вот так вот. Так вот. Не совсем понятно, почему вы в этом, четвертом примере решили (если я ва верно понял), почему не должно быть предупреждений насчет заголовков. Так или иначе вы в любом случае должны послать их самыми первыми, до посыла всяких там текстовых сообщений через echo - банально потому, что именно такого поведения от веб-сервера ожидают браузеры. И php тут просто уведомляет вас, что вы неверно пытаетесь сообщить браузеру что-либо через заголовки. Для понимания этого стоит обратиться к собственно протоколу HTTP, а именно к ег структуре. Ответ веб-сервера в общем случае состоит из трех частей: Starting line, Headers и Message Body. Первое отвечает за тип сообщения, второе за заголовки и третье собственно за то, что получит браузер в качестве ответа. Соответственно, когда приведенный код выше начинает выполняться, вне зависимост от того, играетесь ли вы с буфером или нет, веб-сервер, который работает с вашим интерпретаторо php, будь он apache'ем или чем угодно еще, при получении от php первого же текстовог сообщения в третьей строке вашего кода (а оно будет первым, данный код же будет выполняться последовательно), сформирует ответ браузеру, который будет состоять из трех перечисленных выше частей. И далее веб-браузер может всего лишь досылать что-либо в Message Body, но заголовки уже будут отправлены. Другими словами, если вы хотите что-то вывести в заголовках, то вам нужно сформироват их до первого любого вывода в браузер, или же выстроить логику своего приложения каким-либо иным образом, где функция header() не будет использоваться после любой функции, реализующей вывод чего бы то ни было браузеру посредством HTTP.

Ответ 2



Чтобы немного прояснить для себя и других, как работает буферизация в PHP, давайте заглянем в исходники. У меня исходники php-7.1.3. Заходим в корневую папку исходников и пытаемся найти в них реализацию функции ob_get_contents: $ fgrep -r 'PHP_FUNCTION(ob_get_contents)' . Находится такое: ./main/output.c:PHP_FUNCTION(ob_get_contents) Вот эта функция: PHP_FUNCTION(ob_get_contents) { if (zend_parse_parameters_none() == FAILURE) { return; } if (php_output_get_contents(return_value) == FAILURE) { RETURN_FALSE; } } (Попутно было замечено, что PHP-еретики в своих кодах используют табулятор вместо 4-х пробелов.) В том же файле находим функцию php_output_get_contents: PHPAPI int php_output_get_contents(zval *p) { if (OG(active)) { ZVAL_STRINGL(p, OG(active)->buffer.data, OG(active)->buffer.used); return SUCCESS; } else { ZVAL_NULL(p); return FAILURE; } } Что происходит? Выглядит так, как будто что-то (видимо, возвращаемая строка) формируетс на основе некоторого адреса в памяти и длины данных. То есть пользовательский буфер это просто кусок памяти. Вероятно, там еще есть стек меток для каждого вызова ob_start. Это уже сами ищите реализацию ob_start, кому интересно. Аналогичным образом можно найти реализацию функции flush и увидеть, что она работает через функцию конкретного применяемого SAPI. Я посмотрел SAPI для PHP как модуля Apache: static void php_apache_sapi_flush(void *server_context) { php_struct *ctx; request_rec *r; ctx = server_context; /* If we haven't registered a server_context yet, * then don't bother flushing. */ if (!server_context) { return; } r = ctx->r; sapi_send_headers(); r->status = SG(sapi_headers).http_response_code; SG(headers_sent) = 1; if (ap_rflush(r) < 0 || r->connection->aborted) { php_handle_aborted_connection(); } } Здесь используются структуры C API самого Apache, то есть и вывод предварительн должен делаться таким же образом. Как Apache буферизует выводимые данные - это уже его личное дело и к PHP не имеет никакого отношения. Результат таков, что "системная" и "пользовательская" буферизация в PHP - это абсолютн разные вещи, настолько разные, что их не стоило бы называть в одном списке, чтобы не вносить сумятицу, и, возможно, с той же целью, пользовательскую буферизацию вообще стоило бы назвать как-то по-другому.

вторник, 12 марта 2019 г.

Буферизация stdout, работа fflush

Что делает fflush? Многие пишут,что эта функция дает команду ОС сбросить содержимое буфера на диск. Но разве этим занимается ОС? Насколько я понял при работе с файлами буфер создает сама программа, например, функция fopen. В таблице syscall-ов linux я не нашел чего-то похожего на flush. При этом в пример приводят вот такой код:
#include int main() { printf("Hello"); while(true); return 0; }
При запуске такой программы, hello напечатано не будет, если не добавить после printf вызов fflush(stdout). Однако вот такой код вполне нормально работает:
#include #include int main() { char b[] = "Hello"; write(1,(void*)b,sizeof(b)-1); while(true); return 0; }
Тут мы не используем высокоуровневые методы работы с stdout, а работаем на уровне syscall-ов. Я правильно понимаю что буферизация работы с файлами реализована средствами самой программы, а не ОС? И при вызове write, данные сразу будут записаны на диск?


Ответ

Что касается fflush, в сущности всё так и есть. Семейство функций для работы с потока в Си (fopen/fread/fprintf итп) fflush сбрасывает только оные буферы. Они же могут быть сброшены и раньше по исчерпанию (размер стандартного буфера BUFSIZ [8к для glibc/linux/x86]) или в зависимости от настроек буферизации (см. man 3 setbuf), в частности по-умолчанию для обычных файлов используется блочная буферизация (сбрасывается по 4к для glibc/linux), для терминалов (втч для stdout — строчная, сбрасывается по символу '
'), для stderr буферизации нет.
Для Linux собственно во время сброса буфера происходит системный вызов write(), а что происходит с данными после сброса этого стандарт Си не гарантирует.
Из man 3 fflush
ЗАМЕЧАНИЯ Заметим, что fflush() сбрасывает буферы только пользовательского пространства, заданные библиотекой Си. Чтобы гарантировать, что данные действительно физически сохранены на диске, буферы ядра также должны быть сохранены, например, с помощью вызова sync(2) или fsync(2).
Что происходит с данными после write() зависит от того с каким файлом связан дескриптор и как он был открыт. Практически в любом случае ОС практически всегда проводит дополнительную буферизацию/кеширование. Очевидное исключение, например, запись в /dev/null; в принципе возможно существование устройств не буферизирующих ничего, а работающих напрямую с буфером из юзерспейса, но на вскидку я таких не назову
Не вдаваясь в детали в Linux'е для обычного файла на обычной дисковой ФС при не-синхронном вводе-выводе происходит следующее: ядро вносит изменения в кеше страниц и помечает их как грязные, затем возвращает управление процессу. Далее специальный демон (раньше был один pdflush (с несколькими потоками), ныне для каждого диска/блочного устройства/некоторых ФС свой) следит за грязными страницами и отдаёт команды драйверам ФС сохранить данные на диск при одном из условий:
Когда не хватает свободной памяти Когда грязных страниц слишком много (см. /proc/sys/vm/dirty_{ratio,background_ratio}), По прошествии определённого времени (указанного в /proc/sys/vm/dirty_{writeback_centisecs,expire_seconds}) По вызову [f]sync() Когда ещё какая-то умная эвристика сработала.
В этот момент ФС может преобразовать данные (например сжать или зашифровать). Далее создаётся запрос на вывод на блочное устройство. Когда драйвер жёсткого диска обработает этот запрос т.е. отправит данные непосредственно на устройство и получил подтверждение, что всё в порядке, страница снова отмечается, как чистая.
При синхронном вводе-выводе (когда файл открыт с O_SYNC или ФС смонтирована с -o sync) всё точно также, но управление процессу из write() не будет возвращено пока страница не отмечена как чистая.