The design of the unix operating system by Maurice J

Вид материалаРеферат
Захват файла и записи
5.5 Указание места в файле, где будет выполняться
Подобный материал:
1   ...   8   9   10   11   12   13   14   15   ...   55

│ файла, количество прочитанных байт и адрес для пе- │

│ редачи в пространство пользователя; │

│ освободить буфер; /* заблокированный в алгоритме │

│ bread */ │

│ } │

│ разблокировать индекс; │

│ скорректировать значение смещения в таблице файлов для │

│ следующей операции чтения; │

│ возвратить (общее число прочитанных байт); │

│ } │

L-------------------------------------------------------------


Рисунок 5.5. Алгоритм чтения из файла


за указателем (см. Рисунок 5.3). Затем оно устанавливает значения

нескольких параметров ввода-вывода в адресном пространстве про-

цесса (Рисунок 5.6), тем самым устраняя необходимость в их пере-

даче в качестве параметров функции. В частности, ядро

указывает в качестве режима ввода-вывода "чтение", устанавливает

флаг, свидетельствующий о том, что ввод-вывод направляется в ад-

ресное пространство пользователя, значение поля счетчика байтов

приравнивает количеству байт, которые будут прочитаны, устанавли-

вает адрес пользовательского буфера данных и, наконец, значение

смещения (из таблицы файлов), равное смещению в байтах внутри

файла до места, откуда начинается ввод-вывод. После того, как яд-

ро установит значения параметров ввода-вывода в адресном прост-

ранстве процесса, оно обращается к индексу, используя указатель

из таблицы файлов, и блокирует его прежде, чем начать чтение из

файла.

Затем в алгоритме начинается цикл, выполняющийся до тех пор,

пока операция чтения не будет произведена до конца. Ядро преобра-

зует смещение в байтах внутри файла в номер блока, используя ал-


-------------------------------------------------------┐

│ mode чтение или запись │

│ count количество байт для чтения или записи │

│ offset смещение в байтах внутри файла │

│ address адрес места, куда будут копироваться данные,│

│ в памяти пользователя или ядра │

│ flag отношение адреса к памяти пользователя или │

│ к памяти ядра │

L-------------------------------------------------------


Рисунок 5.6. Параметры ввода-вывода, хранящиеся в пространс-

тве процесса


горитм bmap, и вычисляет смещение внутри блока до места, откуда

следует начать ввод-вывод, а также количество байт, которые будут

прочитаны из блока. После считывания блока в буфер, возможно, с

продвижением (алгоритмы bread и breada) ядро копирует данные из

блока по назначенному адресу в пользовательском процессе. Оно

корректирует параметры ввода-вывода в адресном пространстве про-

цесса в соответствии с количеством прочитанных байт, увеличивая

значение смещения в байтах внутри файла и адрес места в пользова-

тельском процессе, куда будет доставлена следующая порция данных,

и уменьшая число байт, которые необходимо прочитать, чтобы выпол-

нить запрос пользователя. Если запрос пользователя не удовлетво-

рен, ядро повторяет весь цикл, преобразуя смещение в байтах внут-

ри файла в номер блока, считывая блок с диска в системный буфер,

копируя данные из буфера в пользовательский процесс, освобождая

буфер и корректируя значения параметров ввода-вывода в адресном

пространстве процесса. Цикл завершается, либо когда ядро выполнит

запрос пользователя полностью, либо когда в файле больше не будет

данных, либо если ядро обнаружит ошибку при чтении данных с диска

или при копировании данных в пространство пользователя. Ядро кор-

ректирует значение смещения в таблице файлов в соответствии с ко-

личеством фактически прочитанных байт; поэтому успешное выполне-

ние операций чтения выглядит как последовательное считывание

данных из файла. Системная операция lseek (раздел 5.6) устанавли-

вает значение смещения в таблице файлов и изменяет порядок, в ко-

тором процесс читает или записывает данные в файле.


-------------------------------------------------------┐

│ #include

│ main() │

│ { │

│ int fd; │

│ char lilbuf[20],bigbuf[1024]; │

│ │

│ fd = open("/etc/passwd",O_RDONLY); │

│ read(fd,lilbuf,20); │

│ read(fd,bigbuf,1024); │

│ read(fd,lilbuf,20); │

│ } │

L-------------------------------------------------------


Рисунок 5.7. Пример программы чтения из файла


Рассмотрим программу, приведенную на Рисунке 5.7. Функция

open возвращает дескриптор файла, который пользователь засылает в

переменную fd и использует в последующих вызовах функции read.

Выполняя функцию read, ядро проверяет, правильно ли задан пара-

метр "дескриптор файла", а также был ли файл предварительно отк-

рыт процессом для чтения. Оно сохраняет значение адреса пользова-

тельского буфера, количество считываемых байт и начальное смеще-

ние в байтах внутри файла (соответственно: lilbuf, 20 и 0), в

пространстве процесса. В результате вычислений оказывается, что

нулевое значение смещения соответствует нулевому блоку файла, и

ядро возвращает точку входа в индекс, соответствующую нулевому

блоку. Предполагая, что такой блок существует, ядро считывает

полный блок размером 1024 байта в буфер, но по адресу lilbuf ко-

пирует только 20 байт. Оно увеличивает смещение внутри пространс-

тва процесса на 20 байт и сбрасывает счетчик данных в 0. Посколь-

ку операция read выполнилась, ядро переустанавливает значение

смещения в таблице файлов на 20, так что последующие операции

чтения из файла с данным дескриптором начнутся с места, располо-

женного со смещением 20 байт от начала файла, а системная функция

возвращает число байт, фактически прочитанных, т.е. 20.

При повторном вызове функции read ядро вновь проверяет кор-

ректность указания дескриптора и наличие соответствующего файла,

открытого процессом для чтения, поскольку оно никак не может уз-

нать, что запрос пользователя на чтение касается того же самого

файла, существование которого было установлено во время последне-

го вызова функции. Ядро сохраняет в пространстве процесса пользо-

вательский адрес bigbuf, количество байт, которые нужно прочитать

процессу (1024), и начальное смещение в файле (20), взятое из

таблицы файлов. Ядро преобразует смещение внутри файла в номер

дискового блока, как раньше, и считывает блок. Если между вызова-

ми функции read прошло непродолжительное время, есть шансы, что

блок находится в буферном кеше. Однако, ядро не может полностью

удовлетворить запрос пользователя на чтение за счет содержимого

буфера, поскольку только 1004 байта из 1024 для данного запроса

находятся в буфере. Поэтому оно копирует оставшиеся 1004 байта из

буфера в пользовательскую структуру данных bigbuf и корректирует

параметры в пространстве процесса таким образом, чтобы следующий

шаг цикла чтения начинался в файле с байта 1024, при этом данные

следует копировать по адресу байта 1004 в bigbuf в объеме 20

байт, чтобы удовлетворить запрос на чтение.

Теперь ядро переходит к началу цикла, содержащегося в алго-

ритме read. Оно преобразует смещение в байтах (1024) в номер ло-

гического блока (1), обращается ко второму блоку прямой адреса-

ции, номер которого хранится в индексе, и отыскивает точный

дисковый блок, из которого будет производиться чтение. Ядро счи-

тывает блок из буферного кеша или с диска, если в кеше данный

блок отсутствует. Наконец, оно копирует 20 байт из буфера по

уточненному адресу в пользовательский процесс. Прежде чем выйти

из системной функции, ядро устанавливает значение поля смещения в

таблице файлов равным 1044, то есть равным значению смещения в

байтах до места, куда будет производиться следующее обращение. В

последнем вызове функции read из примера ядро ведет себя, как и в

первом обращении к функции, за исключением того, что чтение из

файла в данном случае начинается с байта 1044, так как именно это

значение будет обнаружено в поле смещения той записи таблицы фай-

лов, которая соответствует указанному дескриптору.

Пример показывает, насколько выгодно для запросов ввода-выво-

да работать с данными, начинающимися на границах блоков файловой

системы и имеющими размер, кратный размеру блока. Это позволяет

ядру избегать дополнительных итераций при выполнении цикла в ал-

горитме read и всех вытекающих последствий, связанных с дополни-

тельными обращениями к индексу в поисках номера блока, который

содержит данные, и с конкуренцией за использование буферного пу-

ла. Библиотека стандартных модулей ввода-вывода создана таким об-

разом, чтобы скрыть от пользователей размеры буферов ядра; ее ис-

пользование позволяет избежать потерь производительности,

присущих процессам, работающим с небольшими порциями данных,

из-за чего их функционирование на уровне файловой системы неэф-

фективно (см. упражнение 5.4).

Выполняя цикл чтения, ядро определяет, является ли файл объ-

ектом чтения с продвижением: если процесс считывает последова-

тельно два блока, ядро предполагает, что все очередные операции

будут производить последовательное чтение, до тех пор, пока не

будет утверждено обратное. На каждом шаге цикла ядро запоминает

номер следующего логического блока в копии индекса, хранящейся в

памяти, и на следующем шаге сравнивает номер текущего логического

блока со значением, запомненным ранее. Если эти номера равны, яд-

ро вычисляет номер физического блока для чтения с продвижением и

сохраняет это значение в пространстве процесса для использования

в алгоритме breada. Конечно же, пока процесс не считал конец бло-

ка, ядро не запустит алгоритм чтения с продвижением для следующе-

го блока.

Обратившись к Рисунку 4.9, вспомним, что номера некоторых

блоков в индексе или в блоках косвенной адресации могут иметь ну-

левое значение, пусть даже номера последующих блоков и ненулевые.

Если процесс попытается прочитать данные из такого блока, ядро

выполнит запрос, выделяя произвольный буфер в цикле read, очищая

его содержимое и копируя данные из него по адресу пользователя.

Этот случай не имеет ничего общего с тем случаем, когда процесс

обнаруживает конец файла, говорящий о том, что после этого места

запись информации никогда не производилась. Обнаружив конец фай-

ла, ядро не возвращает процессу никакой информации (см. упражне-

ние 5.1).

Когда процесс вызывает системную функцию read, ядро блокирует

индекс на время выполнения вызова. Впоследствии, этот процесс мо-

жет приостановиться во время чтения из буфера, ассоциированного с

данными или с блоками косвенной адресации в индексе. Если еще

одному процессу дать возможность вносить изменения в файл в то

время, когда первый процесс приостановлен, функция read может

возвратить несогласованные данные. Например, процесс может счи-

тать из файла несколько блоков; если он приостановился во время

чтения первого блока, а второй процесс собирался вести запись в

другие блоки, возвращаемые данные будут содержать старые данные

вперемешку с новыми. Таким образом, индекс остается заблокирован-

ным на все время выполнения вызова функции read для того, чтобы

процессы могли иметь целостное видение файла, то есть видение то-

го образа, который был у файла перед вызовом функции.

Ядро может выгружать процесс, ведущий чтение, в режим задачи

на время между двумя вызовами функций и планировать запуск других

процессов. Так как по окончании выполнения системной функции с

индекса снимается блокировка, ничто не мешает другим процессам

обращаться к файлу и изменять его содержимое. Со стороны системы

было бы несправедливо держать индекс заблокированным все время от

момента, когда процесс открыл файл, и до того момента, когда файл

будет закрыт этим процессом, поскольку тогда один процесс будет

держать все время файл открытым, тем самым не давая другим про-

цессам возможности обратиться к файлу. Если файл имеет имя "/etc/

passwd", то есть является файлом, используемым в процессе регист-

рации для проверки пользовательского пароля, один пользователь

может умышленно (или, возможно, неумышленно) воспрепятствовать

регистрации в системе всех остальных пользователей. Чтобы пре-

дотвратить возникновение подобных проблем, ядро снимает с индекса

блокировку по окончании выполнения каждого вызова системной функ-

ции, использующей индекс. Если второй процесс внесет изменения в

файл между двумя вызовами функции read, производимыми первым про-

цессом, первый процесс может прочитать непредвиденные данные, од-

нако структуры данных ядра сохранят свою согласованность.

Предположим, к примеру, что ядро выполняет два процесса, кон-

курирующие между собой (Рисунок 5.8). Если допустить, что оба

процесса выполняют операцию open до того, как любой из них вызы-

вает системную функцию read или write, ядро может выполнять функ-

ции чтения и записи в любой из шести последовательностей: чте-

ние1, чтение2, запись1, запись2, или чтение1, запись1, чтение2,

запись2, или чтение1, запись1, запись2, чтение2 и т.д. Состав ин-

формации, считываемой процессом A, зависит от последовательности,

в которой система выполняет функции, вызываемые двумя процессами;

система не гарантирует, что данные в файле останутся такими же,

какими они были после открытия файла. Использование возможности

захвата файла и записей (раздел 5.4) позволяет процессу гаранти-


-------------------------------------------------------------┐

│ #include

│ /* процесс A */ │

│ main() │

│ { │

│ int fd; │

│ char buf[512]; │

│ fd = open("/etc/passwd",O_RDONLY); │

│ read(fd,buf,sizeof(buf)); /* чтение1 */ │

│ read(fd,buf,sizeof(buf)); /* чтение2 */ │

│ } │

│ │

│ /* процесс B */ │

│ main() │

│ { │

│ int fd,i; │

│ char buf[512]; │

│ for (i = 0; i < sizeof(buf); i++) │

│ buf[i] = 'a'; │

│ fd = open("/etc/passwd",O_WRONLY); │

│ write(fd,buf,sizeof(buf)); /* запись1 */ │

│ write(fd,buf,sizeof(buf)); /* запись2 */ │

│ } │

L-------------------------------------------------------------


Рисунок 5.8. Процессы, ведущие чтение и запись


ровать сохранение целостности файла после его открытия.

Наконец, программа на Рисунке 5.9 показывает, как процесс мо-

жет открывать файл более одного раза и читать из него, используя

разные файловые дескрипторы. Ядро работает со значениями смещений

в таблице файлов, ассоциированными с двумя файловыми дескриптора-

ми, независимо, и поэтому массивы buf1 и buf2 будут по завершении

выполнения процесса идентичны друг другу при условии, что ни один

процесс в это время не производил запись в файл "/etc/passwd".


5.3 WRITE


Синтаксис вызова системной функции write (писать):

number = write(fd,buffer,count);

где переменные fd, buffer, count и number имеют тот же смысл, что

и для вызова системной функции read. Алгоритм записи в обычный

файл похож на алгоритм чтения из обычного файла. Однако, если

в файле отсутствует блок, соответствующий смещению в байтах до

места, куда должна производиться запись, ядро выделяет блок, ис-

пользуя алгоритм alloc, и присваивает ему номер в соответствии с

точным указанием места в таблице содержимого индекса. Если смеще-

ние в байтах совпадает со смещением для блока косвенной адреса-

ции, ядру, возможно, придется выделить несколько блоков для ис-

пользования их в качестве блоков косвенной адресации и информаци-


-------------------------------------------------------------┐

│ #include

│ main() │

│ { │

│ int fd1,fd2; │

│ char buf1[512],buf2[512]; │

│ │

│ fd1 = open("/etc/passwd",O_RDONLY); │

│ fd2 = open("/etc/passwd",O_RDONLY); │

│ read(fd1,buf1,sizeof(buf1)); │

│ read(fd2,buf2,sizeof(buf2)); │

│ } │

L-------------------------------------------------------------


Рисунок 5.9. Чтение из файла с использованием двух дескрипторов


онных блоков. Индекс блокируется на все время выполнения функции

write, так как ядро может изменить индекс, выделяя новые блоки;

разрешение другим процессам обращаться к файлу может разрушить

индекс, если несколько процессов выделяют блоки одновременно, ис-

пользуя одни и те же значения смещений. Когда запись завершается,

ядро корректирует размер файла в индексе, если файл увеличился в

размере.

Предположим, к примеру, что процесс записывает в файл байт с

номером 10240, наибольшим номером среди уже записанных в файле.

Обратившись к байту в файле по алгоритму bmap, ядро обнаружит,

что в файле отсутствует не только соответствующий этому байту

блок, но также и нужный блок косвенной адресации. Ядро назначает

дисковый блок в качестве блока косвенной адресации и записывает

номер блока в копии индекса, хранящейся в памяти. Затем оно выде-

ляет дисковый блок под данные и записывает его номер в первую по-

зицию вновь созданного блока косвенной адресации.

Так же, как в алгоритме read, ядро входит в цикл, записывая

на диск по одному блоку на каждой итерации. При этом на каждой

итерации ядро определяет, будет ли производиться запись целого

блока или только его части. Если записывается только часть блока,

ядро в первую очередь считывает блок с диска для того, чтобы не

затереть те части, которые остались без изменений, а если записы-

вается целый блок, ядру не нужно читать весь блок, так как в лю-

бом случае оно затрет предыдущее содержимое блока. Запись осу-

ществляется поблочно, однако ядро использует отложенную запись

(раздел 3.4) данных на диск, запоминая их в кеше на случай, если

они понадобятся вскоре другому процессу для чтения или записи, а

также для того, чтобы избежать лишних обращений к диску. Отложен-

ная запись, вероятно, наиболее эффективна для каналов, так как

другой процесс читает канал и удаляет из него данные (раздел

5.12). Но даже для обычных файлов отложенная запись эффективна,

если файл создается временно и вскоре будет прочитан. Например,

многие программы, такие как редакторы и электронная почта, созда-

ют временные файлы в каталоге "/tmp" и быстро удаляют их. Исполь-

зование отложенной записи может сократить количество обращений к

диску для записи во временные файлы.


ЗАХВАТ ФАЙЛА И ЗАПИСИ


В первой версии системы UNIX, разработанной Томпсоном и Ричи,

отсутствовал внутренний механизм, с помощью которого процессу мог

бы быть обеспечен исключительный доступ к файлу. Механизм захвата

был признан излишним, поскольку, как отмечает Ричи, "мы не имеем

дела с большими базами данных, состоящими из одного файла, кото-

рые поддерживаются независимыми процессами" (см. [Ritchie 81]).

Для того, чтобы повысить привлекательность системы UNIX для ком-

мерческих пользователей, работающих с базами данных, в версию V

системы ныне включены механизмы захвата файла и записи. Захват

файла - это средство, позволяющее запретить другим процессам про-

изводить чтение или запись любой части файла, а захват записи -

это средство, позволяющее запретить другим процессам производить

ввод-вывод указанных записей (частей файла между указанными сме-

щениями). В упражнении 5.9 рассматривается реализация механизма

захвата файла и записи.


5.5 УКАЗАНИЕ МЕСТА В ФАЙЛЕ, ГДЕ БУДЕТ ВЫПОЛНЯТЬСЯ

ВВОД-ВЫВОД - LSEEK


Обычное использование системных функций read и write обеспе-

чивает последовательный доступ к файлу, однако процессы могут ис-