struct inode *ip = fp->f_inode;
struct buf *bp;
daddr_t bno; // очередной блок файла
// dev - устройство,
// интерфейсом которого является файл-устройство,
// или на котором расположен обычный файл.
dev_t dev = (ip->i_mode & (IFCHR|IFBLK)) ?
А. Богатырев, 1992-95 - 248 - Си в UNIX
ip->i_rdev : ip->i_dev;
switch( ip->i_mode & IFMT ){
case IFCHR: // байто-ориентированное устройство
(*cdevsw[major(dev)].d_read)(minor(dev));
// прочие параметры передаются через u-area
break;
case IFREG: // обычный файл
case IFDIR: // каталог
case IFBLK: // блочно-ориентированное устройство
do{
bno = bmap(ip, fp->f_offset /*RWptr*/, u_count);
if(u_pbsize==0 || (long)bno < 0) break; // EOF
bp = bread(dev, bno); // block read
iomove(bp->b_addr + u_pboff, u_pbsize, B_READ);
Функция iomove копирует данные
bp->b_addr[ u_pboff..u_pboff+u_pbsize-1 ]
из адресного пространства ядра (из буфера в ядре) в адресное пространство процесса по
адресам
u_base[ 0..u_pbsize-1 ]
то есть пересылает u_pbsize байт между ядром и процессом (u_base попадает в iomove
через статическую переменную). При записи вызовом write(), iomove с флагом B_WRITE
производит обратное копирование - из памяти процесса в память ядра. Продолжим:
// продвинуть счетчики и указатели:
u_count -= u_pbsize;
u_base += u_pbsize;
fp->f_offset += u_pbsize; // RWptr
} while( u_count != 0 );
break;
...
return( srccount - u_count );
} // end read
Теперь обсудим некоторые места этого алгоритма. Сначала посмотрим, как происходит
обращение к байтовому устройству. Вместо адресов блоков мы получаем код устройства
i_rdev. Коды устройств в UNIX (тип dev_t) представляют собой пару двух чисел, назы-
ваемых мажор и минор, хранимых в старшем и младшем байтах кода устройства:
#define major(dev) ((dev >> 8) & 0x7F)
#define minor(dev) ( dev & 0xFF)
Мажор обозначает тип устройства (диск, терминал, и.т.п.) и приводит к одному из драй-
веров (если у нас есть 8 терминалов, то их обслуживает один и тот же драйвер); а
минор обозначает номер устройства данного типа (... каждый из терминалов имеет миноры
0..7). Миноры обычно служат индексами в некоторой таблице структур внутри выбранного
драйвера. Мажор же служит индексом в переключательной таблице устройств. При этом
блочно-ориентированные устройства выбираются в одной таблице - bdevsw[], а байто-
ориентированные - в другой - cdevsw[] (см. ; имена таблиц означают
block/character device switch). Каждая строка таблицы содержит адреса функций,
выполняющих некоторые предопределенные операции способом, зависимым от устройства.
Сами эти функции реализованы в драйверах устройств. Аргументом для этих функций
обычно служит минор устройства, к которому производится обращение. Функция в
А. Богатырев, 1992-95 - 249 - Си в UNIX
драйвере использует этот минор как индекс для выбора конкретного экземпляра уст-
ройства данного типа; как индекс в массиве управляющих структур (содержащих текущее
состояние, режимы работы, адреса функций прерываний, адреса очередей данных и.т.п.
каждого конкретного устройства) для данного типа устройств. Эти управляющие структуры
различны для разных типов устройств (и их драйверов).
Каждая строка переключательной таблицы содержит адреса функций, выполняющих опе-
рации open, close, read, write, ioctl, select. open служит для инициализации уст-
ройства при первом его открытии (++ip->i_count==1) - например, для включения мотора;
close - для выключения при последнем закрытии (--ip->i_count==0). У блочных уст-
ройств поля для read и write объединены в функцию strategy, вызываемую с параметром
B_READ или B_WRITE. Вызов ioctl предназначен для управления параметрами работы уст-
ройства. Операция select - для опроса: есть ли поступившие в устройство данные (нап-
ример, есть ли в clist-е ввода с клавиатуры байты? см. главу "Экранные библиотеки").
Вызов select применим только к некоторым байтоориентированным устройствам и сетевым
портам (socket-ам). Если данное устройство не умеет выполнять такую операцию, то
есть запрос к этой операции должен вернуть в программу ошибку (например, операция
read неприменима к принтеру), то в переключательной таблице содержится специальное
имя функции nodev; если же операция допустима, но является фиктивной (как write для
/dev/null) - имя nulldev. Обе эти функции-заглушки представляют собой "пустышки":
{}.
Теперь обратимся к блочно-ориентированным устройствам. UNIX использует внутри
ядра дополнительную буферизацию при обменах с такими устройствами[*]. Использованная
нами выше функция bp=bread(dev,bno); производит чтение физического блока номер bno с
устройства dev. Эта операция обращается к драйверу конкретного устройства и вызывает
чтение блока в некоторую область памяти в ядре ОС: в один из кэш-буферов (cache,
"запасать"). Заголовки кэш-буферов (struct buf) организованы в список и имеют поля
(см. файл ):
b_dev
код устройства, с которого прочитан блок;
b_blkno
номер физического блока, хранящегося в буфере в данный момент;
b_flags
флаги блока (см. ниже);
b_addr
адрес участка памяти (как правило в самом ядре), в котором собственно и хранится
содержимое блока.
Буферизация блоков позволяет системе экономить число обращений к диску. При обраще-
нии к bread() сначала происходит поиск блока (dev,bno) в таблице кэш-буферов. Если
блок уже был ранее прочитан в кэш, то обращения к диску не происходит, поскольку
копия содержимого дискового блока уже есть в памяти ядра. Если же блока еще нет в
кэш-буферах, то в ядре выделяется чистый буфер, в заголовке ему прописываются нужные
значения полей b_dev и b_blkno, и блок считывается в буфер с диска вызовом функции
bp->b_flags |= B_READ; // род работы: прочитать
(*bdevsw[major(dev)].d_startegy)(bp);
// bno и минор - берутся из полей *bp
из драйвера конкретного устройства.
Когда мы что-то изменяем в файле вызовом write(), то изменения на самом деле
происходят в кэш-буферах в памяти ядра, а не сразу на диске. При записи в блок буфер
помечается как измененный:
b_flags |= B_DELWRI; // отложенная запись
____________________
[*] Следует отличать эту системную буферизацию от буферизации при помощи библиотеки
stdio. Библиотека создает буфер в самом процессе, тогда как системные вызовы имеют
буфера внутри ядра.
А. Богатырев, 1992-95 - 250 - Си в UNIX
и на диск немедленно не записывается. Измененные буфера физически записываются на
диск в таких случаях:
- Был сделан системный вызов sync();
- Ядру не хватает кэш-буферов (их число ограничено). Тогда самый старый буфер (к
которому дольше всего не было обращений) записывается на диск и после этого
используется для другого блока.
- Файловая система была отмонтирована вызовом umount;
Понятно, что не измененные блоки обратно на диск из буферов не записываются (т.к. на
диске и так содержатся те же самые данные). Даже если файл уже закрыт close, его
блоки могут быть еще не записаны на диск - запись произойдет лишь при вызове sync.
Это означает, что измененные блоки записываются на диск "массированно" - по многу
блоков, но не очень часто, что позволяет оптимизировать и саму запись на диск: сорти-
ровкой блоков можно достичь минимизации перемещения магнитных головок над диском.
Отслеживание самых "старых" буферов происходит за счет реорганизации списка
заголовков кэш-буферов. В большом упрощении это можно представить так: как только к
блоку происходит обращение, соответствующий заголовок переставляется в начало списка.
В итоге самый "пассивный" блок оказывается в хвосте - он то и переиспользуется при
нужде.
"Подвисание" файлов в памяти ядра значительно ускоряет работу программ, т.к.
работа с памятью гораздо быстрее, чем с диском. Если блок надо считать/записать, а он
уже есть в кэше, то реального обращения к диску не происходит. Зато, если случится
сбой питания (или кто-то неаккуратно выключит машину), а некоторые буфера еще не были
сброшены на диск - то часть изменений в файлах будет потеряна. Для принудительной
записи всех измененных кэш-буферов на диск существует сисвызов "синхронизации" содер-
жимого дисков и памяти
sync(); // synchronize
Вызов sync делается раз в 30 секунд специальным служебным процессом /etc/update,
запускаемым при загрузке системы. Для работы с файлами, которые должны гарантиро-
ванно быть корректными на диске, используется открытие файла
fd = open( имя, O_RDWR | O_SYNC);
которое означает, что при каждом write блок из кэш-буфера немедленно записывается на
диск. Это делает работу надежнее, но существенно медленнее.
Специальные файлы устройств не могут быть созданы вызовом creat, создающим
только обычные файлы. Файлы устройств создаются вызовом mknod:
#include
dev_t dev = makedev(major, minor);
/* (major << 8) | minor */
mknod( имяФайла, кодыДоступа|тип, dev);
где dev - пара (мажор,минор) создаваемого устройства; кодыДоступа - коды доступа к
файлу (0777)[**]; тип - это одна из констант S_IFIFO, S_IFCHR, S_IFBLK из include-файла
.
mknod доступен для выполнения только суперпользователю (за исключением случая
S_IFIFO). Если бы это было не так, то можно было бы создать файл устройства, связан-
ный с существующим диском, и читать информацию с него напрямую, в обход механизмов
логической файловой системы и защиты файлов кодами доступа.
Можно создать файл устройства с мажором и/или минором, не отвечающим никакому
реальному устройству (нет такого драйвера или минор слишком велик). Открытие таких
____________________
[**] Обычно к блочным устройствам (дискам) доступ разрешается только суперпользова-
телю, в противном случае можно прочитать с "сырого" диска (в обход механизмов файло-
вой системы) физические блоки любого файла и весь механизм защиты окажется неработаю-
щим.
А. Богатырев, 1992-95 - 251 - Си в UNIX
устройств выдает код ошибки ENODEV.
Из нашей программы мы можем вызовом stat() узнать код устройства, на котором
расположен файл. Он будет содержаться в поле dev_t st_dev; а если файл является спе-
циальным файлом (интерфейсом драйвера устройства), то код самого этого устройства
можно узнать из поля dev_t st_rdev; Рассмотрим пример, который выясняет, относятся ли
два имени к одному и тому же файлу:
#include
#include
void main(ac, av) char *av[]; {
struct stat st1, st2; int eq;
if(ac != 3) exit(13);
stat(av[1], &st1); stat(av[2], &st2);
if(eq =
(st1.st_ino == st2.st_ino && /* номера I-узлов */
st1.st_dev == st2.st_dev)) /* коды устройств */
printf("%s и %s - два имени одного файла\n",av[1],av[2]);
exit( !eq );
}
Наконец, вернемся к склейке нескольких файловых систем в одну объединенную иерархию:
ino=2
*------ корневая файловая система
/ \ /\ на диске /dev/hd0
/ /\ /\
\
*-/mnt/hd1
:
* ino=2 FS на диске /dev/hd1
/ \ (removable FS)
/\ \