Системное программирование Линукс. Linux. Системное программирование. Вступление
Скачать 0.65 Mb.
|
96 на опережающее считывание, пока процесс обрабатывает первый фрагмент полу ченных данных. Если (как часто бывает) процесс должен вотвот выдать новый запрос на считывание последующего фрагмента, то ядро может передать данные от исходного опережающего считывания, даже не запрашивая лишний раз дисковый вводвывод. Ядро управляет опережающим считыванием динамически, как и работой со страничным кэшем. Если система заметит, что процесс постоянно использует данные, полученные в ходе опережающего считывания, то ядро увеличивает окно такого считывания, забирая с диска все больше и больше дополнительных данных. Окно опережающего считывания может быть совсем маленьким (16 Кбайт), но в некоторых случаях достигает и 128 Кбайт. Верно и обратное: если ядро замеча ет, что опережающее считывание не дает значительного эффекта, это означает, что приложение выбирает данные из файла в произвольном, а не последователь ном порядке. В таком случае опережающее считывание может быть полностью отключено. Работа страничного кэша должна быть совершенно прозрачной. Как правило, системные программисты не могут оптимизировать свой код так, чтобы он учитывал существование страничного кэша и задействовал его на пользу программе. Един ственный вариант использования таких преимуществ предполагает, что программист реализует подобный кэш самостоятельно уже в пользовательском пространстве. Как правило, для оптимального использования страничного кэша требуется просто писать эффективный код. Опять же можно пользоваться опережающим считыва нием. Последовательный файловый вводвывод всегда предпочтительнее произ вольного доступа, хотя организовать его удается не всегда. Страничная отложенная запись Как уже упоминалось в разд. «Запись с помощью write()», ядро откладывает опе рации записи, используя систему буферов. Когда процесс выдает запрос на запись, данные копируются в буфер, который с этого момента считается грязным. Это означает, что копия информации, находящаяся в памяти, новее копии, имеющейся на диске. После этого запрос на запись сразу возвращается. Если был сделан другой запрос на запись, обращенный к тому же фрагменту файла, то буфер заполняется новыми данными. Если к одному и тому же файлу обращено несколько запросов записи, все они генерируют новые буферы. Рано или поздно информация из грязных буферов должна быть сброшена на диск, синхронизируя, таким образом, дисковые файлы с данными, находящимися в памяти. Этот процесс называется отложенной записью. Она происходит в двух случаях. Когда объем свободной памяти становится ниже определенного порога (эту величину можно конфигурировать), содержимое грязных буферов записыва ется Глава 2. Файловый ввод-вывод 97 на диск. Очищенные таким образом буферы можно удалить и высвободить часть памяти. Если возраст буфера превышает определенный порог (эту величину можно конфигурировать), информация из данного буфера записывается на диск. Бла годаря этому данные не остаются в грязном буфере на неопределенно долгий срок. Отложенная запись выполняется группой потоков ядра, которые называются промывочными. Если удовлетворяется хотя бы одно из двух вышеназванных условий, то такие потоки активизируются и начинают сбрасывать информацию из грязных буферов на диск. Это происходит, пока оба условия не станут лож ными. В некоторых ситуациях сразу несколько промывочных потоков одновременно начинают выполнять отложенную запись. Это делается, чтобы максимально эф фективно задействовать сильные стороны параллелизма и для реализации техни ки избегания скученности. Выполняя ее, мы стараемся исключить накопление за писей в период ожидания отдельно взятого блочного устройства. Если имеются грязные буферы, относящиеся к разным блочным устройствам, то промывочные потоки будут работать с расчетом на максимальное использование каждого из блочных устройств. Таким образом компенсируется один недостаток, имевшийся в сравнительно старых ядрах: предшественники промывочных потоков (потоки pdflush , а еще раньше bdflush ) могли потратить все свое время, дожидаясь един ственного блочного устройства, тогда как другие блочные устройства простаивали. На современных машинах ядро Linux может одновременно подпитывать множе ство дисков. Буферы представлены в ядре структурой данных buffer_head . Она отслеживает различные метаданные, ассоциированные с буфером, в частности информацию о том, чист данный буфер или грязен. Кроме того, в этой структуре содержится указатель на сами данные. Они находятся в страничном кэше. Так обеспечивается объединение подсистемы буферов и страничного кэша. В ранних версиях ядра Linux — до 2.4 — подсистема буферов была отделена от страничного кэша, существовал как страничный, так и буферный кэш. Таким образом, данные могли одновременно находиться и в буферном кэше (в грязном буфере), и в страничном (в качестве кэшированных данных). Естественно, синхро низация этих двух отдельных кэшей потребовала определенных усилий. Единый страничный кэш, появившийся в ядре Linux 2.4, был встречен восторженными отзывами. Отложенная запись и действующая в Linux подсистема буферов обеспечивают быструю запись, но повышают риск потери данных при резком сбое питания. Для устранения такой опасности в критически важных приложениях (а также приложениях программистовпараноиков) можно использовать синхронизирован ный вводвывод (о нем мы говорили выше в этой главе). Внутренняя организация ядра 98 Резюме В данной главе мы обсудили одну из фундаментальных тем системного програм мирования в Linux — вводвывод. В таких системах, как Linux, которые стремятся представить все, что можно, в виде файлов, очень важно уметь правильно открывать, считывать, записывать и закрывать последние. Все эти операции относятся к клас сике UNIX и описаны во многих стандартах. В следующей главе мы поговорим о буферизованном вводевыводе и стандарт ных интерфейсах вводавывода, предоставляемых в библиотеке C. Стандартная библиотека С используется в данном случае не только для удобства программиста: буферизация вводавывода в пользовательском пространстве позволяет значитель но повысить производительность. Глава 2. Файловый ввод-вывод 100 Ввод-вывод с пользовательским буфером Программы, которым приходится выполнять множество мелких системных вызо вов к обычным файлам, часто осуществляют вводвывод с пользовательским бу фером. Так называется буферизация, выполняемая в пользовательском простран стве. Ее можно организовывать в приложении вручную либо прозрачно выполнять в библиотеке. Однако такая буферизация никак не связана с ядром. Как было ска зано в гл. 2, на внутрисистемном уровне буферизация данных в ядре происходит посредством отложенных записей, объединения смежных запросов вводавывода и опережающего считывания. Пользовательская буферизация выполняется иначе, но также нацелена на улучшение производительности. Рассмотрим пример с использованием программы dd , работающей в пользова тельском пространстве: dd bs=1 count=2097152 if=/dev/zero of=pirate Мы имеем аргумент bs=1 , поэтому данная команда будет копировать 2 Мбайт информации с устройства /dev/zero (это виртуальное устройство, выдающее беско нечное количество нулей) в файл pirate . Операция будет передана в виде 2 097 152 од нобайтовых фрагментов. Таким образом, на копирование этих данных мы затратим примерно 2 000 000 операций считывания и записи — по байту за раз. Рассмотрим копирование тех же 2 Мбайт информации, но уже с использовани ем блоков размером по 1024 байт каждый: dd bs=1024 count=2048 if=/dev/zero of=pirate Эта операция позволяет скопировать те же 2 Мбайт информации в тот же файл, но требует в 1024 раза меньше операций считывания и записи. Налицо радикальное улучшение производительности, как видно из табл. 3.1. В ней я записал затраченное время (измеренное тремя разными способами) четырьмя командами dd , которые отличались лишь размером блока. Реальное время — это общее время, истекшее на настенных часах. Пользовательское время — это время, потраченное на исполнение программного кода в пользовательском пространстве. Системное время — это время, затраченное на выполнение инициированных процессом системных вызовов в пространстве ядра. Таблица 3.1. Влияние размера блока на производительность Размер блока, байты Реальное время, секунды Пользовательское время, секунды Системное время, секунды 1 18,707 1,118 17,549 1024 0,025 0,002 0,023 1130 0,035 0,002 0,027 При использовании фрагментов данных по 1024 байт достигается грандиозное улучшение производительности по сравнению с передачей информации по одному байту. Тем не менее данные, приведенные выше (см. табл. 3.1), также свидетель ствуют, что при использовании блоков с большим размером — и, соответственно, при Глава 3. Буферизованный ввод-вывод 101 дальнейшем сокращении количества системных вызовов — производительность может и снижаться, если используемые при работе фрагменты не кратны размеру блока диска. Несмотря на то что запросы 1130байтовых фрагментов требуют меньшего количества системных вызовов, они оказываются менее эффективными, чем 1024байтовые запросы, кратные размеру блока. Чтобы обратить эти аспекты производительности себе на пользу, необходимо заранее знать размер физического блока на данном устройстве. Результаты, при веденные выше (см. табл. 3.1), показывают, что размер блока, скорее всего, будет равен 1024 байт, целочисленному кратному 1024 или делителю 1024. В случае с /dev/zero точный размер блока равен 4096 байт. Размер блока. На практике размер блока обычно составляет 512, 1024, 2048, 4096 или 8192 байт. Как показано выше (см. табл. 3.1), для значительного повышения производи тельности достаточно просто выполнять операции фрагментами, которые являют ся целочисленными кратными или делителями размера блока. Дело в том, что и ядро, и аппаратное обеспечение работает в контексте блоков. Соответственно, если опе рировать либо размером блока, либо значением, которое аккуратно вписывается в блок, то все запросы вводавывода будут выполняться в пределах целых блоков. Лишняя работа в ядре исключается. Узнать размер блока на конкретном устройстве довольно просто: для этого исполь зуется системный вызов stat() , рассмотренный в гл. 8, либо команда stat(1) . Однако оказывается, что в большинстве случаев не требуется знать точный размер блока. Выбирая размер для будущих операций вводавывода, главное — не получить какуюнибудь заведомо неровную величину, например 1130. Ни один блок в исто рии UNIX не имел размер 1130 байт — если вы выберете такой объем для ваших операций, то уже после первого запроса выравнивание вводавывода будет нару шено. Если же выбранный вами размер операции позволяет сохранять выравнива ние по границам блоков, то производительность будет высокой. Чем больше крат ное, тем меньше системных вызовов вам потребуется. Соответственно, самый простой вариант — использовать при вводевыводе достаточно большой буфер, являющийся общим кратным типичных размеров блоков. Очень удобны значения 4096 и 8192 байт. Получается, достаточно осуществлять весь вводвывод фрагментами по 4 или 8 Кбайт, и проблемы решены? Не все так просто. Проблема заключается в том, что сами программы редко оперируют цельными блоками — они работают с отдельны ми полями, строками, символами, а не с такой абстракцией, как блок. Вводвывод с пользовательским буфером устраняет разрыв между файловой системой, рабо тающей с блоками, и приложением, оперирующим собственными абстракциями. Принцип работы пользовательского буфера одновременно простой и мощный: по мере того как данные записываются, они сохраняются в буфере в пределах адрес ного пространства конкретной программы. Когда размер буфера достигает уста новленного предела, называемого размером буфера, содержимое этого буфера пе реносится на диск за одну операцию записи. Считывание данных также происходит фрагментами, равными размеру буфера и выровненными по границам блоков. Ввод-вывод с пользовательским буфером 102 Поступающие от приложения запросы на считывание имеют разные размеры и об служиваются не из файловой системы напрямую, а удовлетворяются фрагментами, получаемыми через буфер. По мере того как приложение считывает все больше и больше информации, данные выдаются из буфера кусками. Наконец, как только буфер пустеет, начинается считывание следующего сравнительно крупного фраг мента, выровненного по границам блоков. Таким образом, приложение может считывать и записывать любые произвольные фрагменты данных, как потребуется, но буферизация данных всегда выполняется лишь сравнительно большими куска ми. Эти крупные операции, выровненные по границам блоков, уже направляются в файловую систему. В конечном итоге нам удается обойтись меньшим количеством системных вызовов при работе со значительными объемами данных, выровненны ми строго по границам блоков. В результате мы имеем серьезное повышение про изводительности. Можете самостоятельно реализовать пользовательскую буферизацию в ваших программах. Кстати, именно так и делается во многих критически важных прило жениях. Однако в абсолютном большинстве программ используется популярная стандартная библиотека ввода-вывода, входящая в состав стандартной библиоте ки C, либо, как вариант, библиотека классов ввода-вывода языка C++, с помощью которых легко создавать надежные и практичные решения с пользовательской буферизацией. Стандартный ввод-вывод В составе стандартной библиотеки C есть стандартная библиотека вводавывода, иногда сокращенно именуемая stdio . Она, в свою очередь, предоставляет незави симое от платформы решение для пользовательской буферизации. Стандартная библиотека вводавывода проста в использовании, но ей не занимать мощности. В отличие от таких языков программирования, как FORTRAN, язык С не со держит никакой встроенной поддержки ключевых слов, которая обеспечивала бы более сложный функционал, чем управление выполнением программы, арифме тические действия и т. д. Естественно, в языке нет и встроенной поддержки ввода вывода. По эволюции языка программирования С его пользователи разработали стандартные наборы процедур, обеспечивающих основную функциональность. В частности, речь идет о манипуляции строками, математических процедурах, работе с датой и временем, а также о вводевыводе. Со временем эти процедуры совершенствовались. В 1989 году, когда был ратифицирован стандарт ANSIC (C89), все эти функции были объединены в стандартную библиотеку C. В C95, C99 и C11 добавилось несколько новых интерфейсов, однако стандартная библиотека ввода вывода осталась практически нетронутой со времени появления в C89. Оставшаяся часть этой главы посвящена вводувыводу с пользовательским буфером, тому, как он относится к файловому вводувыводу и как реализован в стандартной библиотеке C. Иными словами, нас интересует открытие, закрытие, считывание и запись файлов с помощью стандартной библиотеки C. Решение, Глава 3. Буферизованный ввод-вывод 103 будут ли в приложении использоваться стандартный вводвывод, собственное решение с пользовательским буфером либо обычные системные вызовы, прини мает сам разработчик. Это решение должно быть тщательно взвешенным и прини маться с учетом всех потребностей приложения и его поведения. В стандартах C некоторые детали всегда остаются на усмотрение конкретной реа лизации, и в разных реализациях действительно часто добавляются новые возможно сти. В этой главе, а также во всей оставшейся книге описаны интерфейсы и поведения в виде, в котором они реализованы в библиотеке glibc в современной системе Linux. Если в Linux делается отступление от стандартов, я это особо указываю. Указатели файлов. Процедуры стандартного вводавывода не работают непо средственно с файловыми дескрипторами. Вместо этого каждая использует свой уникальный идентификатор, обычно называемый указателем файла. В библиоте ке C указатель файла ассоциируется с файловым дескриптором (отображается на него). Указатель файла представлен как указатель на определение типа FILE , опре деляемый в ПРИМЕЧАНИЕ Название FILE часто критикуют за то, что оно записывается прописными буквами, что кажется особенно непривлекательным в стандарте C, ведь в нем (а следовательно, и в большинстве стилей написания кода в конкретных приложениях) имена типов и функций записываются только в нижнем регистре. Эта странность объясняется историческими причинами: изначально стандартный ввод- вывод был написан в виде макрокоманд. Не только FILE, но и все методы библиотеки были реали- зованы в виде наборов макрокоманд. Сегодня также сохраняется традиция давать всем макроко- мандам названия прописными буквами. По мере того как язык C развивался и стандартный ввод-вывод наконец был регламентирован как официальная часть языка, большинство методов были переписаны в виде обычных функций, а FILE стал определением типа, но он так и продолжа- ет записываться в верхнем регистре. В терминологии вводавывода открытый файл называется потоком данных 1 Потоки могут быть открыты для чтения (поток данных ввода), для записи (поток данных вывода) или для того и другого (поток данных ввода/вывода). Открытие файлов Файлы открываются для чтения или записи с помощью функции fopen() : #include FILE * fopen (const char *path, const char *mode); Эта функция открывает файл path , поведение которого определено в mode , и ас социирует с ним новый поток данных. 1 В русском языке сложилась омонимия между понятиями «поток» (thread) и «поток» (stream). Во избежание двусмысленности в этом разделе слово stream будет переводить ся как «поток данных», а слово thread — как «программный поток». Данная терминоло гия сохраняется вплоть до конца главы. — Примеч. пер. Открытие файлов |