связанных с дополнительными обращениями к индексу в поисках номера блока,
который содержит данные, и с конкуренцией за использование буферного пула.
Библиотека стандартных модулей ввода-вывода создана таким образом, чтобы
скрыть от пользователей размеры буферов ядра; ее использование позволяет из-
бежать потерь производительности, присущих процессам, работающим с небольши-
ми порциями данных, из-за чего их функционирование на уровне файловой систе-
мы неэффективно (см. упражнение 5.4).
Выполняя цикл чтения, ядро определяет, является ли файл объектом чтения
с продвижением: если процесс считывает последовательно два блока, ядро пред-
полагает, что все очередные операции будут производить последовательное чте-
ние, до тех пор, пока не будет утверждено обратное. На каждом шаге цикла яд-
ро запоминает номер следующего логического блока в копии индекса, хранящейся
в памяти, и на следующем шаге сравнивает номер текущего логического блока со
значением, запомненным ранее. Если эти номера равны, ядро вычисляет номер
физического блока для чтения с продвижением и сохраняет это значение в прос-
транстве процесса для использования в алгоритме breada. Конечно же, пока
процесс не считал конец блока, ядро не запустит алгоритм чтения с продвиже-
нием для следующего блока.
Обратившись к Рисунку 4.9, вспомним, что номера некоторых блоков в ин-
дексе или в блоках косвенной адресации могут иметь нулевое значение, пусть
даже номера последующих блоков и ненулевые. Если процесс попытается прочи-
тать данные из такого блока, ядро выполнит запрос, выделяя произвольный бу-
фер в цикле read, очищая его содержимое и копируя данные из него по адресу
пользователя. Этот случай не имеет ничего общего с тем случаем, когда про-
цесс обнаруживает конец файла, говорящий о том, что после этого места запись
информации никогда не производилась. Обнаружив конец файла, ядро не возвра-

93

щает процессу никакой информации (см. упражнение 5.1).
Когда процесс вызывает системную функцию read, ядро блокирует индекс на
время выполнения вызова. Впоследствии, этот процесс может приостановиться во
время чтения из буфера, ассоциированного с данными или с блоками косвенной
адресации в индексе. Если еще одному процессу дать возможность вносить изме-
нения в файл в то время, когда первый процесс приостановлен, функция read
может возвратить несогласованные данные. Например, процесс может считать из
файла несколько блоков; если он приостановился во время чтения первого бло-
ка, а второй процесс собирался вести запись в другие блоки, возвращаемые
данные будут содержать старые данные вперемешку с новыми. Таким образом, ин-
декс остается заблокированным на все время выполнения вызова функции read
для того, чтобы процессы могли иметь целостное видение файла, то есть виде-
ние того образа, который был у файла перед вызовом функции.
Ядро может выгружать процесс, ведущий чтение, в режим задачи на время
между двумя вызовами функций и планировать запуск других процессов. Так как
по окончании выполнения системной функции с индекса снимается блокировка,
ничто не мешает другим процессам обращаться к файлу и изменять его содержи-
мое. Со стороны системы было бы несправедливо держать индекс заблокированным
все время от момента, когда процесс открыл файл, и до того момента, когда
файл будет закрыт этим процессом, поскольку тогда один процесс будет держать
все время файл открытым, тем самым не давая другим процессам возможности об-
ратиться к файлу. Если файл имеет имя "/etc/ passwd", то есть является фай-
лом, используемым в процессе регистрации для проверки пользовательского па-
роля, один пользователь может умышленно (или, возможно, неумышленно) воспре-
пятствовать регистрации в системе всех остальных пользователей. Чтобы пре-
дотвратить возникновение подобных проблем, ядро снимает с индекса блокировку
по окончании выполнения каждого вызова системной функции, использующей ин-
декс. Если второй процесс внесет изменения в файл между двумя вызовами функ-
ции read, производимыми первым процессом, первый процесс может прочитать
непредвиденные данные, однако структуры данных ядра сохранят свою согласо-
ванность.
Предположим, к примеру, что ядро выполняет два процесса, конкурирующие

+------------------------------------------------------------+
| #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 */ |
| } |
+------------------------------------------------------------+

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

94



между собой (Рисунок 5.8). Если допустить, что оба процесса выполняют опера-
цию open до того, как любой из них вызывает системную функцию read или
write, ядро может выполнять функции чтения и записи в любой из шести после-
довательностей: чтение1, чтение2, запись1, запись2, или чтение1, запись1,
чтение2, запись2, или чтение1, запись1, запись2, чтение2 и т.д. Состав ин-
формации, считываемой процессом A, зависит от последовательности, в которой
система выполняет функции, вызываемые двумя процессами; система не гаранти-
рует, что данные в файле останутся такими же, какими они были после открытия
файла. Использование возможности захвата файла и записей (раздел 5.4) позво-
ляет процессу гарантировать сохранение целостности файла после его открытия.

Наконец, программа на Рисунке 5.9 показывает, как процесс может откры-
вать файл более одного раза и читать из него, используя разные файловые дес-
крипторы. Ядро работает со значениями смещений в таблице файлов, ассоцииро-
ванными с двумя файловыми дескрипторами, независимо, и поэтому массивы buf1
и buf2 будут по завершении выполнения процесса идентичны друг другу при ус-
ловии, что ни один процесс в это время не производил запись в файл
"/etc/passwd".


    5.3 WRIТЕ



Синтаксис вызова системной функции 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)); |
| } |
+------------------------------------------------------------+

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


онных блоков. Индекс блокируется на все время выполнения функции write, так
как ядро может изменить индекс, выделяя новые блоки; разрешение другим про-
цессам обращаться к файлу может разрушить индекс, если несколько процессов
выделяют блоки одновременно, используя одни и те же значения смещений. Когда
запись завершается, ядро корректирует размер файла в индексе, если файл уве-

95

личился в размере.
Предположим, к примеру, что процесс записывает в файл байт с номером
10240, наибольшим номером среди уже записанных в файле. Обратившись к байту
в файле по алгоритму bmap, ядро обнаружит, что в файле отсутствует не только
соответствующий этому байту блок, но также и нужный блок косвенной адреса-
ции. Ядро назначает дисковый блок в качестве блока косвенной адресации и за-
писывает номер блока в копии индекса, хранящейся в памяти. Затем оно выделя-
ет дисковый блок под данные и записывает его номер в первую позицию вновь
созданного блока косвенной адресации.
Так же, как в алгоритме read, ядро входит в цикл, записывая на диск по
одному блоку на каждой итерации. При этом на каждой итерации ядро определя-
ет, будет ли производиться запись целого блока или только его части. Если
записывается только часть блока, ядро в первую очередь считывает блок с дис-
ка для того, чтобы не затереть те части, которые остались без изменений, а
если записывается целый блок, ядру не нужно читать весь блок, так как в лю-
бом случае оно затрет предыдущее содержимое блока. Запись осуществляется
поблочно, однако ядро использует отложенную запись (раздел 3.4) данных на
диск, запоминая их в кеше на случай, если они понадобятся вскоре другому
процессу для чтения или записи, а также для того, чтобы избежать лишних об-
ращений к диску. Отложенная запись, вероятно, наиболее эффективна для кана-
лов, так как другой процесс читает канал и удаляет из него данные (раздел
5.12). Но даже для обычных файлов отложенная запись эффективна, если файл
создается временно и вскоре будет прочитан. Например, многие программы, та-
кие как редакторы и электронная почта, создают временные файлы в каталоге
"/tmp" и быстро удаляют их. Использование отложенной записи может сократить
количество обращений к диску для записи во временные файлы.


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



В первой версии системы UNIX, разработанной Томпсоном и Ричи, отсутство-
вал внутренний механизм, с помощью которого процессу мог бы быть обеспечен
исключительный доступ к файлу. Механизм захвата был признан излишним, пос-
кольку, как отмечает Ричи, "мы не имеем дела с большими базами данных, сос-
тоящими из одного файла, которые поддерживаются независимыми процессами"
(см. [Ritchie 81]). Для того, чтобы повысить привлекательность системы UNIX
для коммерческих пользователей, работающих с базами данных, в версию V сис-
темы ныне включены механизмы захвата файла и записи. Захват файла - это
средство, позволяющее запретить другим процессам производить чтение или за-
пись любой части файла, а захват записи - это средство, позволяющее запре-
тить другим процессам производить ввод-вывод указанных записей (частей файла
между указанными смещениями). В упражнении 5.9 рассматривается реализация
механизма захвата файла и записи.


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



Обычное использование системных функций read и write обеспечивает после-
довательный доступ к файлу, однако процессы могут использовать вызов систем-
ной функции lseek для указания места в файле, где будет производиться
ввод-вывод, и осуществления произвольного доступа к файлу. Синтаксис вызова
системной функции:

position = lseek(fd,offset,reference);

где fd - дескриптор файла, идентифицирующий файл, offset - смещение в бай-
тах, а reference указывает, является ли значение offset смещением от начала
файла, смещением от текущей позиции ввода-вывода или смещением от конца фай-
ла. Возвращаемое значение, position, является смещением в байтах до места,

96

где будет начинаться следующая операция чтения или записи. Например, в прог-
рамме, приведенной на Рисунке 5.10, процесс открывает файл, считывает байт,
а затем вызывает функцию lseek, чтобы заменить значение поля смещения в таб-
лице файлов величиной, равной 1023 (с переменной reference, имеющей значение
1), и выполняет цикл. Таким образом, программа считывает каждый 1024-й байт
файла. Если reference имеет значение 0, ядро осуществляет поиск от начала
файла, а если 2, ядро ведет поиск от конца файла. Функция lseek ничего не
должна делать, кроме операции поиска, которая позиционирует головку чте-
ния-записи на указанный дисковый сектор. Для того, чтобы выполнить функцию
lseek, ядро просто выбирает значение смещения из таблицы файлов; в последую-
щих вызовах функций read и write смещение из таблицы файлов используется в
качестве начального смещения.


    5.6 CLOSЕ



Процесс закрывает открытый файл, когда процессу больше не нужно обра-
щаться к нему. Синтаксис вызова системной функции close (закрыть):

+--------------------------------------------------------+
| #include |
| main(argc,argv) |
| int argc; |
| char *argv[]; |
| { |
| int fd,skval; |
| char c; |
| |
| if(argc != 2) |
| exit(); |
| fd = open(argv[1],O_RDONLY); |
| if (fd == -1) |
| exit(); |
| while ((skval = read(fd,&c,1)) == 1) |
| { |
| printf("char %c\n",c); |
| skval = lseek(fd,1023L,1); |
| printf("new seek val %d\n",skval); |
| } |
| } |
+--------------------------------------------------------+

Рисунок 5.10. Программа, содержащая вызов системной функции lseek


close(fd);
где fd - дескриптор открытого файла. Ядро выполняет операцию закрытия, ис-
пользуя дескриптор файла и информацию из соответствующих записей в таблице
файлов и таблице индексов. Если счетчик ссылок в записи таблицы файлов имеет
значение, большее, чем 1, в связи с тем, что были обращения к функциям dup
или fork, то это означает, что на запись в таблице файлов делают ссылку дру-
гие пользовательские дескрипторы, что мы увидим далее; ядро уменьшает значе-
ние счетчика и операция закрытия завершается. Если счетчик ссылок в таблице
файлов имеет значение, равное 1, ядро освобождает запись в таблице и индекс
в памяти, ранее выделенный системной функцией open (алгоритм iput). Если
другие процессы все еще ссылаются на индекс, ядро уменьшает значение счетчи-
ка ссылок на индекс, но оставляет индекс процессам; в противном случае ин-
декс освобождается для переназначения, так как его счетчик ссылок содержит
0. Когда выполнение системной функции close завершается, запись в таблице

97

пользовательских дескрипторов файла становится пустой. Попытки процесса ис-
пользовать данный дескриптор заканчиваются ошибкой до тех пор, пока дескрип-
тор не будет переназначен другому файлу в результате выполнения другой сис-
темной функции. Когда процесс завершается, ядро проверяет наличие активных
пользовательских дескрипторов файла, принадлежавших процессу, и закрывает
каждый из них. Таким образом, ни один процесс не может оставить файл откры-
тым после своего завершения.
На Рисунке 5.11, например, показаны записи из таблиц, приведенных на Ри-
сунке 5.4, после того, как второй процесс закрывает соответствующие им фай-
лы. Записи, соответствующие дескрипторам 3 и 4 в таблице пользовательских

пользовательские дескрип-
торы файла таблица файлов таблица индексов
+---------+ +------------+ +--------------+
0| | | | | - |
+---------+ | | | - |
1| | | | | - |
+---------+ +------------+ | - |
2| | | - | | - |
+---------+ | - | | - |
3| ----+----+ | - | | - |
+---------+ | | - | +--------------+
4| ----+---+| | - | +---->| счет- |
+---------+ || | - | | | чик (/etc/ |
5| ----+--+|| +------------+ | +-->| 2 passwd)|
+---------+ ||| | счет- | | | +--------------+
| - | ||+-->| чик +--+ | | - |
| - | || | 1 | | | - |
| - | || +------------+ | | - |
+---------+ || | - | | | - |
|| | - | | | - |
+---------+ || | - | | | - |
0| | || +------------+ | | - |
+---------+ || | счет- | | | - |
1| | |+--->| чик +----|+ | - |
+---------+ | | 1 | || | - |
2| | | +------------+ || | - |
+---------+ | | - | || +--------------+
3| NULL | | | - | || | счет- |
+---------+ | | - | |+->| чик (local)|
4| NULL | | | - | | | 1 |
+---------+ | | - | | +--------------+
5| | | | - | | | - |
+---------+ | +------------+ | | - |
| - | | | счетчик 0 | | | - |
+---------+ | +------------+ | | - |
| | - | | +--------------+
| | - | | | счет- |
| +------------+ | | чик (private)|
| | счетчик 1 | | | 0 |
+---->| +----+ +--------------+
+------------+ | - |
| - | +--------------+
| - |
+------------+
| счетчик 0 |
+------------+

Рисунок 5.11. Таблицы после закрытия файла

98



дескрипторов файлов, пусты. Счетчики в записях таблицы файлов теперь имеют
значение 0, а сами записи пусты. Счетчики ссылок на файлы "/etc/passwd" и
"private" в индексах также уменьшились. Индекс для файла "private" находится
в списке свободных индексов, поскольку счетчик ссылок на него равен 0, но
запись о нем не пуста. Если еще какой-нибудь процесс
обратится к файлу "private", пока индекс еще находится в списке свободных
индексов, ядро востребует индекс обратно, как показано в разделе 4.1.2.


    5.7 СОЗДАНИЕ ФАЙЛА



Системная функция open дает процессу доступ к существующему файлу, а
системная функция creat создает в системе новый файл. Синтаксис вызова сис-
темной функции creat:

fd = creat(pathname,modes);

где переменные pathname, modes и fd имеют тот же смысл, что и в системной
функции open. Если прежде такого файла не существовало, ядро создает новый
файл с указанным именем и указанными правами доступа к нему; если же такой
файл уже существовал, ядро усекает файл (освобождает все существующие блоки

+------------------------------------------------------------+
| алгоритм creat |
| входная информация: имя файла |
| установки прав доступа к файлу |
| выходная информация: дескриптор файла |
| { |
| получить индекс для данного имени файла (алгоритм namei);|
| если (файл уже существует) |
| { |
| если (доступ не разрешен) |
| { |
| освободить индекс (алгоритм iput); |
| возвратить (ошибку); |
| } |
| } |
| в противном случае /* файл еще не существует */ |
| { |
| назначить свободный индекс из файловой системы (алго- |
| ритм ialloc); |
| создать новую точку входа в родительском каталоге: |
| включить имя нового файла и номер вновь назначенного |
| индекса; |
| } |
| выделить для индекса запись в таблице файлов, инициализи-|
| ровать счетчик; |
| если (файл существовал к моменту создания) |
| освободить все блоки файла (алгоритм free); |
| снять блокировку (с индекса); |
| возвратить (пользовательский дескриптор файла); |
| } |
+------------------------------------------------------------+


Рисунок 5.12. Алгоритм создания файла


99

данных и устанавливает размер файла равным 0) при наличии соответствующих
прав доступа к нему (***). На Рисунке 5.12 приведен алгоритм создания файла.
Ядро проводит синтаксический анализ имени пути поиска, используя алго-
ритм namei и следуя этому алгоритму буквально, когда речь идет о разборе
имен каталогов. Однако, когда дело касается последней компоненты имени пути
поиска, а именно идентификатора создаваемого файла, namei отмечает смещение
в байтах до первой
пустой позиции в каталоге и запоминает это смещение в пространстве процесса.
Если ядро не обнаружило в каталоге компоненту имени пути поиска, оно в ко-
нечном счете впишет имя компоненты в только что найденную пустую позицию.
Если в каталоге нет пустых позиций, ядро запоминает смещение до конца ката-
лога и создает новую позицию там. Оно также запоминает в пространстве про-
цесса индекс просматриваемого каталога и держит индекс заблокированным; ка-
талог становится по отношению к новому файлу родительским каталогом. Ядро не
записывает пока имя нового файла в каталог, так что в случае возникновения
ошибок ядру приходится меньше переделывать. Оно проверяет наличие у процесса
разрешения на запись в каталог. Поскольку процесс будет производить запись в
каталог в результате выполнения функции creat, наличие разрешения на запись
в каталог означает, что процессам дозволяется создавать файлы в каталоге.
Предположив, что под данным именем ранее не существовало файла, ядро
назначает новому файлу индекс, используя алгоритм ialloc (раздел 4.6). Затем
оно записывает имя нового файла и номер вновь выделенного индекса в роди-
тельский каталог, а смещение в байтах сохраняет в пространстве процесса.
Впоследствии ядро освобождает индекс родительского каталога, удерживаемый с
того времени, когда в каталоге производился поиск имени файла. Родительский
каталог теперь содержит имя нового файла и его индекс. Ядро записывает вновь
выделенный индекс на диск (алгоритм bwrite), прежде чем записать на диск ка-
талог с новым именем. Если между операциями записи индекса и каталога прои-
зойдет сбой системы, в итоге окажется, что выделен индекс, на который не
ссылается ни одно из имен путей поиска в системе, однако система будет функ-
ционировать нормально. Если, с другой стороны, каталог был записан раньше
вновь выделенного индекса и сбой системы произошел между ними, файловая сис-
тема будет содержать имя пути поиска, ссылающееся на неверный индекс (более
подробно об этом см. в разделе 5.16.1).
Если данный файл уже существовал до вызова функции creat, ядро обнаружи-
вает его индекс во время поиска имени файла. Старый файл должен позволять
процессу производить запись в него, чтобы можно было создать "новый" файл с
тем же самым именем, так как ядро изменяет содержимое файла при выполнении
функции creat: оно усекает файл, освобождая все информационные блоки по ал-
горитму free, так что файл будет выглядеть как вновь созданный. Тем не ме-
нее, владелец и права доступа к файлу остаются прежними: ядро не передает
право собственности на файл владельцу процесса и игнорирует права доступа,
указанные процессом в вызове функции. Наконец, ядро не проверяет наличие
разрешения на запись в каталог, являющийся родительским для существующего
файла, поскольку оно не меняет содержимого каталога.
Функция creat продолжает работу, выполняя тот же алгоритм, что и функция
open. Ядро выделяет созданному файлу запись в таблице файлов, чтобы процесс
мог читать из файла, а также запись в таблице пользовательских дескрипторов
файла, и в конце концов возвращает указатель на последнюю запись в виде
пользовательского дескриптора файла.



---------------------------------------
(***) Системная функция open имеет два флага, O_CREAT (создание) и O_TRUNC
(усечение). Если процесс устанавливает в вызове функции флаг O_CREAT и
файл не существует, ядро создаст файл. Если файл уже существует, он не
будет усечен, если только не установлен флаг O_TRUNC.


100

    5.8 СОЗДАНИЕ СПЕЦИАЛЬНЫХ ФАЙЛОВ



Системная функция mknod создает в системе специальные файлы, в число ко-
торых включаются поименованные каналы, файлы устройств и каталоги. Она похо-
жа на функцию creat в том, что ядро выделяет для файла индекс. Синтаксис вы-
зова системной функции mknod:

mknod(pathname,type and permissions,dev)

где pathname - имя создаваемой вершины в иерархической структуре файловой
системы, type and permissions - тип вершины (например, каталог) и права дос-
тупа к создаваемому файлу, а dev указывает старший и младший номера устройс-
тва для блочных и символьных специальных файлов (глава 10). На Рисунке 5.13
приведен алгоритм, реализуемый функцией mknod при создании новой вершины.
+------------------------------------------------------------+
| алгоритм создания новой вершины |
| входная информация: вершина (имя файла) |
| тип файла |
| права доступа |
| старший, младший номера устройства |
| (для блочных и символьных специальных |
| файлов) |
| выходная информация: отсутствует |
| { |
| если (новая вершина не является поименованным каналом |
| и пользователь не является суперпользователем) |
| возвратить (ошибку); |
| получить индекс вершины, являющейся родительской для |
| новой вершины (алгоритм namei); |
| если (новая вершина уже существует) |
| { |
| освободить родительский индекс (алгоритм iput); |
| возвратить (ошибку); |
| } |
| назначить для новой вершины свободный индекс из файловой|
| системы (алгоритм ialloc); |
| создать новую запись в родительском каталоге: включить |
| имя новой вершины и номер вновь назначенного индекса; |
| освободить индекс родительского каталога (алгоритм |