Текст книги "Операционная система UNIX"
Автор книги: Андрей Робачевский
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 26 (всего у книги 39 страниц)
На рис. 4.14 представлена схема выполнения операций ввода/вывода с использованием буферного кэша. Важной особенностью этой подсистемы является то, что она обеспечивает независимое выполнение операций чтения или записи данных процессом как результат соответствующих системных вызовов, а также фактический обмен данными с периферийным устройством.
Рис. 4.14. Схема работы буферного кэша
Когда процессу требуется прочитать или записать данные он использует системные вызовы read(2) или write(2), направляя тем самым запрос файловой подсистеме. В свою очередь файловая подсистема транслирует этот запрос в запрос на чтение или запись соответствующих дисковых блоков файла и направляет его в буферный кэш. Прежде всего кэш просматривается на предмет наличия требуемого блока в памяти. Если соответствующий буфер найден, его содержимое копируется в адресное пространство процесса в случае чтения и наоборот при записи, и операция завершается. Если блок в кэше не найден, ядро размещает буфер, связывает его с дисковым блоком с помощью заголовка buf
и направляет запрос на чтение драйверу устройства. Обычно используется схема чтения вперед (read-ahead), когда считываются не только запрашиваемые блоки, но и блоки, которые с высокой вероятностью могут потребоваться в ближайшее время (рис. 4.14, а). Таким образом, последующие вызовы read(2) скорее всего не потребуют дискового ввода/вывода, а будут включать лишь копирование данных из буферов в память процесса, – операция, которая, как отмечалось, обладает на несколько порядков большей производительностью (рис. 4.14, б–в). При запросе на модификацию блока изменения также затрагивают только буфер кэша. При этом ядро помечает буфер как «грязный» в заголовке buf (рис. 4.14, г). Перед освобождением такого буфера для повторного использования, его содержимое должно быть предварительно сохранено на диске (рис. 4.14, д).
Перед фактическим использованием буфера, например при чтении или записи буфера процессом, или при операции дискового ввода/вывода, доступ к нему для других процессов должен быть заблокирован. При обращении к уже заблокированному буферу процесс переходит в состояние сна, пока данный ресурс не станет доступным.
Не заблокированные буферы помечаются как свободные и помещаются в специальный список. Буферы в этом списке располагаются в порядке наименее частого использования (Least Recently Used, LRU). Таким образом, когда ядру необходим буфер, оно выбирает тот, к которому не было обращений в течение наиболее продолжительного промежутка времени. После того как работа с буфером завершена, он помещается в конец списка и является наименее вероятным кандидатом на освобождение и повторное использование. Поэтому, если процесс вскоре опять обратится к тому же блоку данных, операция ввода/вывода по-прежнему будет происходить с буфером кэша. С течением времени буфер перемещается в направлении начала очереди, но при каждом последующем обращении к нему, будет помещен в ее конец.
Основной проблемой, связанной с буферным кэшем, является "старение" информации, хранящейся в дисковых блоках, образы которых находятся в буферном кэше. Как следует из схемы работы кэша, большинство изменений затрагивают только данные в соответствующих буферах, в то время, как дисковые блоки хранят уже устаревшую информацию. Разумеется в нормально работающей системе проблемы как таковой не возникает, поскольку в операциях ввода/вывода всегда используются свежие данные буферного кэша. Однако при аварийном останове системы, это может привести к потере изменений данных файлов, сделанных процессами непосредственно перед остановом.
Для уменьшения вероятности таких потерь в UNIX имеется несколько возможностей:
□ Во-первых, может использоваться системный вызов sync(2), который обновляет все дисковые блоки, соответствующие «грязным» буферам. Необходимо отметить, что sync(2) не ожидает завершения операции ввода/вывода, таким образом после возврата из функции не гарантируется, что все «грязные» буферы сохранены на диске.[50]50
В распоряжении администратора имеется командный интерфейс к системному вызову – утилита sync(1M). Поскольку выполнение команды еще не свидетельствует о фактическом завершении ввода/вывода, администраторы практикуют вызов sync(1M) несколько раз. Повторные вызовы повышают вероятность того, что ввод/вывод будет завершен прежде, чем будет введена другая команда или остановлена система, поскольку набор команды занимает определенное время. Тот же эффект может быть достигнут просто ожиданием нескольких секунд после ввода sync(1М), но набор команды позволяет «скрасить ожидание».
[Закрыть]
□ Во-вторых, процесс может открыть файл в синхронном режиме (указав флаг O_SYNC
в системном вызове open(2)). При этом все изменения в файле будут немедленно сохраняться на диске.
□ Наконец, через регулярные промежутки времени в системе пробуждается специальный системный процесс – диспетчер буферного кэша (в различных версиях UNIX его названия отличаются, чаще всего используется fsflush или bdflush). Этот процесс освобождает "грязные" буферы, сохраняя их содержимое в соответствующих дисковых блоках[51]51
Работа диспетчера буферного кэша зависит от версии UNIX и конкретных настроек ядра системы. Например, в SCO UNIX для этого используются несколько параметров. Параметр BDFLUSHR
задает интервал между последовательными пробуждениями bdflush, его значение по умолчанию составляет 30 секунд. Параметр NAUTOUP
задает промежуток времени, который буфер должен оставаться «грязным», прежде чем bdflush сохранит его на диске.
[Закрыть] (рис. 4.14, д).
Центральной концепцией в архитектуре виртуальной памяти SVR4 является отображение файлов. При этом подходе все адресное пространство может быть представлено набором отображений различных файлов в память. Действительно, в страницы памяти, содержащие кодовые сегменты, отображаются соответствующие секции исполняемых файлов. Процесс может задать отображение с помощью системного вызова mmap(2), при этом страницам памяти будут соответствовать определенные участки отображаемого файла. Даже области памяти, содержимое которых изменяется и не связано ни с каким файлом файловой системы, т.н. анонимные страницы, можно отобразить на определенные участки специального файла устройства, отвечающего за область свопинга (именно там сохраняются анонимные объекты памяти). При этом фактический обмен данными между памятью и устройствами их хранения, инициируется возникновением страничной ошибки. Такая архитектура позволяет унифицировать операции ввода/вывода практически для всех случаев.
При этом подходе, когда процесс выполняет вызовы read(2) или write(2), ядро устанавливает отображение части файла, адресованного этими вызовами, в собственное адресное пространство. Затем эта область копируется в адресное пространство процесса. При копировании возникают страничные ошибки, приводящие в фактическому считыванию дисковых блоков файла в память. Поскольку все операции кэширования данных в этом случае обслуживаются подсистемой управления памятью, необходимость в буферном кэше, как отдельной подсистеме, отпадает.
Целостность файловой системы
Значительная часть файловой системы находится в оперативной памяти. А именно, в оперативной памяти расположены суперблок примонтированной системы, метаданные активных файлов (в виде системно-зависимых inode и соответствующих им vnode) даже отдельные блоки хранения данных файлов, временно находящиеся в буферном кэше.
Для операционной системы рассогласование между буферным кэшем и блоками хранения данных отдельных файлов, не приведет к катастрофическим последствиям даже в случае внезапного останова системы, хотя с точки зрения пользователя все может выглядеть иначе. Содержимое отдельных файлов не вносит существенных нарушений в целостность файловой системы.
Другое дело, когда подобные несоответствия затрагивают метаданные файла или другую управляющую информацию файловой системы, например, суперблок. Многие файловые операции затрагивают сразу несколько объектов файловой системы, и если на диске будут сохранены изменения только для части этих объектов, целостность файловой системы может быть существенно нарушена.
Рассмотрим пример создания жесткой связи для файла. Для этого файловой подсистеме необходимо выполнить следующие операции:
1. Создать новую запись в необходимом каталоге, указывающую на inode файла.
2. Увеличить счетчик связей в inode.
Предположим, что аварийный останов системы произошел между первой и второй операциями. В этом случае после запуска в файловой системе будут существовать два имени файла (две записи каталогов), адресующие inode со счетчиком связей di_nlinks
, равным 1. Эта ситуация показана на рис. 4.15 (а). Если теперь будет удалено одно из имен, это приведет к удалению файла как такового, т.е. к освобождению блоков хранения данных и inode, поскольку счетчик связей di_nlinks
станет равным 0. Оставшаяся запись каталога будет указывать на неразмещенный индексный дескриптор, или inode, адресующий уже другой файл (рис. 4.15, б).
Порядок операций с метаданными может иметь существенное влияние на целостность файловой системы. Рассмотрим, например, предыдущий пример. Допустим, порядок операций был изменен и, как и прежде, останов произошел между первой и второй операциями. После запуска системы файл будет иметь лишнюю жесткую связь, но существующая запись каталога останется правильной. Тем не менее при удалении имени файла фактически файл удален не будет, поскольку число связей останется равным 1 (рис. 4.15, в). Хотя это также является ошибкой, результатом которой является засорение дискового пространства, ее последствия все же менее катастрофичны, чем в первом случае.
Рис. 4.15. Нарушение целостности файловой системы
Ядро выбирает порядок совершения операций с метаданными таким образом, чтобы вред от ошибок в случае аварии был минимальным. Однако проблема нарушения этого порядка все же остается, т.к. драйвер может изменять очередность выполнения запросов для оптимизации ввода/вывода. Единственной возможностью сохранить выбранный порядок является синхронизация операций со стороны файловой подсистемы.
В нашем примере файловая подсистема будет ожидать, пока на диск не будет записано содержимое индексного дескриптора, и только после этого произведет изменения каталога.
Отсутствие синхронизации между образом файловой системы в памяти и ее данными на диске в случае аварийного останова может привести к появлению следующих ошибок:
1. Один блок адресуется несколькими mode (принадлежит нескольким файлам).
2. Блок помечен как свободный, но в то же время занят (на него ссылается onode).
3. Блок помечен как занятый, но в то же время свободен (ни один inode на него не ссылается).
4. Неправильное число ссылок в inode (недостаток или избыток ссылающихся записей в каталогах).
5. Несовпадение между размером файла и суммарным размером адресуемых inode блоков.
6. Недопустимые адресуемые блоки (например, расположенные за пределами файловой системы).
7. "Потерянные" файлы (правильные inode, на которые не ссылаются записи каталогов).
8. Недопустимые или неразмещенные номера inode в записях каталогов. Эти ошибки схематически показаны на рис. 4.16.
Рис. 4.16. Возможные ошибки файловой системы
Если нарушение все же произошло, на помощь может прийти утилита fsck(1M), производящая исправление файловой системы. Запуск этой утилиты может производиться автоматически каждый раз при запуске системы, или администратором, с помощью команды:
fsck [options] filesystem
где filesystem
– специальный файл устройства, на котором находится файловая система.
Проверка и исправление должны производиться только на размонтированной файловой системе. Это связано с необходимостью исключения синхронизации таблиц в памяти (ошибочных) с их дисковыми эквивалентами (исправленными). Исключение составляет корневая файловая система, которая не может быть размонтирована. Для ее исправления необходимо использовать опцию обеспечивающую немедленный перезапуск системы после проведения проверки.
Заключение
В этой главе описана организация файловой подсистемы UNIX. Начав разговор с обсуждения архитектуры традиционных файловых систем UNIX, мы остановились на анализе т.н. виртуальной файловой системы, обеспечивающей единый интерфейс доступа к различным типам физических файловых систем.
Мы также рассмотрели, каким образом происходит доступ процесса к данным, хранящимся в файлах, вплотную подошли к разговору о подсистеме ввода/вывода, который и продолжим в следующей главе.
Глава 5
Подсистема ввода/вывода
Фактическая архитектура ввода/вывода скрыта от прикладного процесса несколькими интерфейсами. Один из них – интерфейс файловой системы был рассмотрен в предыдущей главе. Взаимодействие с удаленными ресурсами обеспечивается сетевыми интерфейсами сокетов или TLI (Transport Layer Interface), которые описываются в главе 6. Однако возможны ситуации, когда прикладному процессу требуется взаимодействие с периферийными устройствами на более низком уровне. Хотя в этом случае роль файловой подсистемы не столь велика, как при работе с обычными файлами, все равно ядро предоставляет процессу унифицированную схему, скрывающую истинную архитектуру того или иного устройства.
В конечном итоге работа всех этих интерфейсов, как высокого уровня, (файловая система), так и более низкого (взаимодействие с физическим устройством), обеспечивается подсистемой ввода/вывода ядра операционной системы.
В данной главе мы ознакомимся с архитектурой этой подсистемы, основным компонентом которой являются драйверы – модули ядра, обеспечивающие непосредственную работу с периферийными устройствами. Поскольку характеристики периферийных устройств значительно различаются, то UNIX использует два основных типа драйверов – символьные и блочные. Как следует из названия, драйверы первого типа обеспечивают обмен сравнительно небольшими объемами данных с устройством, что имеет место при работе, например, с терминалами или принтерами. Драйверы второго типа производят передачу данных блоками, что характерно для дисковых носителей данных. Эти типы драйверов входят в традиционную подсистему ввода/вывода и присутствуют во всех версиях UNIX.
Во второй части главы мы подробно остановимся на архитектуре драйверов подсистемы STREAMS, которая является неотъемлемой частью ядра в версиях UNIX System V. Эти драйверы представляют собой отдельный тип, обладающий такими ценными возможностями, как буферизация и управление потоком данных. К подсистеме STREAMS мы также вернемся в следующей главе при обсуждении архитектуры сетевого доступа в UNIX System V.
Драйверы устройств
Драйверы устройств обеспечивают интерфейс между ядром UNIX и аппаратной частью компьютера. Благодаря этому от остальной части ядра скрыты архитектурные особенности компьютера, что значительно упрощает перенос системы и поддержку работы различных периферийных устройств.
В UNIX существует большое количество драйверов. Часть из них обеспечивает доступ к физическим устройствам, например, жесткому диску, принтеру или терминалу, другие предоставляют аппаратно-независимые услуги. Примером последних могут служить драйверы /dev/kmem для работы с виртуальной памятью ядра /dev/null, представляющий «нулевое» устройство.
В процессе запуска системы ядро вызывает соответствующие процедуры инициализации установленных драйверов. Во многих версиях UNIX эти процедуры выводят на консоль сообщение о том, что драйвер найден, и инициализация прошла успешно, а также параметры драйвера и устройства.
Типы драйверовДрайверы различаются по возможностям, которые они предоставляют, а также по тому, каким образом обеспечивается к ним доступ и управление. Можно рассматривать три основные типа драйверов:
Символьные драйверы | Этот тип драйверов обеспечивает работу с устройствами с побайтовым доступом и обменом данными. К таким устройствам можно отнести модемы, терминалы, принтеры, манипуляторы мышь и т.д. Доступ к таким драйверам не включает использование буферного кэша, таким образом ввод и вывод как правило не буферизуется. При необходимости буферизации для символьных драйверов обычно используется подход, основанный на структурах данных, называемых clist. |
Блочные драйверы | Этот тип драйверов позволяет производить обмен данными с устройством фиксированными порциями (блоками). Например, для жесткого диска данные можно адресовать и, соответственно, читать только секторами, размер которых составляет несколько сотен байтов. Для блочных драйверов обычно используется буферный кэш, который и является интерфейсом между файловой системой и устройством. Хотя операции чтения и записи для процесса допускают обмен данными, размер которых меньше размера блока, на системном уровне это все равно приводит к считыванию всего блока, изменению части его данных и записи измененного блока обратно на диск. |
Драйверы низкого уровня (raw drivers) | Этот тип интерфейса блочных драйверов позволяет производить обмен данными с блочными устройствами, минуя буферный кэш. Это, в частности, означает, что устройство может быть адресовано элементами, размер которых не совпадает с размером блока. Обмен данными происходит независимо от файловой подсистемы и буферного кэша, что позволяет ядру производить передачу непосредственно между пользовательским процессом и устройством, без дополнительного копирования. |
На рис. 5.1 приведена упрощенная схема взаимодействия драйверов устройств с другими подсистемами операционной системы UNIX.
Рис. 5.1. Драйверы устройств UNIX
Не все драйверы служат для работы с физическими устройствами, такими как сетевой адаптер, последовательный порт или монитор. Часть драйверов служат для предоставления различных услуг ядра прикладным процессам и не имеют непосредственного отношения к аппаратной части компьютера. Такие драйверы называются программными или драйверами псевдоустройств. Можно привести несколько примеров псевдоустройств и соответствующих им программных драйверов:
/dev/kmem | Обеспечивает доступ к виртуальной памяти ядра. Зная виртуальные адреса внутренних структур ядра, процесс может считывать хранящуюся в них информацию. С помощью этого драйвера может, например, быть реализована версия утилиты ps(1), выводящей информацию о состоянии процессов в системе. |
/dev/ksyms | Обеспечивает доступ к разделу исполняемого файла ядра, содержащего таблицу символов. Совместно с драйвером /dev/kmem обеспечивает удобный интерфейс для анализа внутренних структур ядра. |
/dev/mem | Обеспечивает доступ к физической памяти компьютера. |
/dev/null | Является «нулевым» устройством. При записи в это устройство данные просто удаляются, а при чтении процессу возвращается 0 байтов. Примеры использования этого устройства рассматривались в главе 1, когда с помощью /dev/null мы подавляли вывод сообщений об ошибках. |
/dev/zero | Обеспечивает заполнение нулями указанного буфера. Этот драйвер часто используется для инициализации области памяти. |
Базовая архитектура драйверов
Драйвер устройства адресуется старшим номером (major number) устройства. Напомним, что среди атрибутов специальных файлов устройств, которые обеспечивают пользовательский интерфейс доступа к периферии компьютера, это число присутствует наряду с другим, также имеющим отношение к драйверу, – младшим номером (minor number). Младший номер интерпретируется самим драйвером (например, для клонов, оно задает старшее число устройства, которое требуется «размножить»). Другим примером использования младших номеров может служить драйвер диска. В то время как доступ к любому из разделов диска осуществляется одним и тем же драйвером и, соответственно, через один и тот же старший номер, младший номер указывает, к какому именно разделу требуется обеспечить доступ.
Доступ к драйверу осуществляется ядром через специальную структуру данных (коммутатор устройств), каждый элемент которой содержит указатели на соответствующие функции драйвера – точки входа. Старшее число, по существу, является указателем на элемент коммутатора устройств, обеспечивая, тем самым, ядру возможность вызова необходимой функции указанного драйвера. Таким образом, коммутатор устройств определяет базовый интерфейс драйвера устройств.
Этот интерфейс различен для блочных и символьных устройств. Ядро содержит коммутаторы устройств двух типов: bdevsw для блочных и cdevsw для символьных устройств. Ядро размещает отдельный массив для каждого типа коммутатора, и любой драйвер устройства имеет запись в соответствующем массиве. Если драйвер обеспечивает как блочный, так и символьный интерфейсы, его точки входа будут представлены в обоих массивах.
Типичное описание этих двух массивов имеет следующий вид (назначение различных точек входа мы рассмотрим далее в этом разделе):
struct bdevsw[] {
int (*d_open)();
int (*d_close)();
int (*d_strategy)();
int (*d_size)();
int (*d_xhalt)();
...
} bdevsw[];
struct cdevsw[] {
int (*d_open)();
int (*d_close)();
int (*d_read)();
int (*d_write)();
int (*d_ioctl)();
int (*d_xpoll)();
int (*d_xhalt)();
struct streamtab *d_str;
...
} cdevsw[];
Ядро вызывает функцию open()
требуемого драйвера следующим образом:
(*bdevsw[getmajor(dev)].d_open)(dev, ...);
передавая ей в качестве одного из параметров переменную dev
(типа dev_t
), содержащую старший и младший номера. Макрос getmajor()
служит для извлечения старшего номера из переменной dev
. Благодаря этому драйвер имеет возможность определить, с каким младшим номером была вызвана функция open()
, и выполнить соответствующие действия.
Коммутатор определяет абстрактный интерфейс драйвера устройства. Каждый драйвер обеспечивает соответствующую реализацию функций этого интерфейса. Если драйвер не поддерживает каких-либо функций стандартного интерфейса, он заменяет соответствующие точки входа специальными заглушками, предоставляемыми ядром. Когда ядру требуется запросить какую-либо операцию у драйвера устройства, оно определяет элемент коммутатора, соответствующий данному драйверу (используя его старший номер), и вызывает требуемую функцию.
В названиях точек входа драйвера используются определенные соглашения. Поскольку в ядре системы одновременно присутствует большое количество различных драйверов, каждый их них должен иметь уникальное имя во избежание проблем при компиляции (точнее, при редактировании связей) ядра. Каждый драйвер имеет уникальное двухсимвольное обозначение, используемое в качестве префикса названий функций. Например, драйвер виртуальной памяти ядра /dev/kmem имеет префикс mm, таким образом функции этого драйвера будут иметь названия mmopen()
, mmclose()
, mmread()
и mmwrite()
.
В табл. 5.1 приведены некоторые точки входа, общие для различных типов драйверов, а символами xx, с которых начинается имя каждой функции, обозначен уникальный префикс драйвера. Стандартные точки входа драйвера отличаются для разных версий UNIX. Например, некоторые версии имеют расширенный коммутатор блочных устройств, включающий такие функции, как xxioctl()
, xxread()
и xxwrite()
. В некоторых версиях включены точки входа для инициализации и сброса шины данных.
Таблица 5.1. Типичные точки входа в драйвер устройства
xxopen() | + | + | + | Вызывается при каждой операции открытии устройства. Обеспечивает необходимую реинициализацию физического устройства и внутренних данных драйвера. Например, для каждого последующего открытия драйвера могут размещаться дополнительные буферы, обеспечивающие возможность независимой работы с устройством нескольким процессам. |
xxclose() | + | + | + | Вызывается, когда число ссылок на данный драйвер становится равным нулю, т. е. ни один из процессов системы не работает с устройством (не имеет открытым соответствующий файл устройства). Может вызывать отключение физического устройства. Например, драйвер накопителя на магнитной ленте может перемотать ленту в начало. |
xxread() | + | - | + | Производит чтение данных от устройства. |
xxwrite() | + | - | + | Производит запись данных на устройство. |
xxioctl() | + | - | + | Является общим интерфейсом управления устройством. Драйвер может определить набор команд, которые могут быть переданы ему, например с помощью системного вызова ioctl(2). |
xxintr() | + | + | + | Вызывается при поступлении прерывания, связанного с данным устройством. Может выполнить копирование данных от устройства в промежуточные буферы, которые затем считываются функцией xxread() по запросу прикладного процесса. |
xxpoll() | + | - | + | Производит опрос устройства. Обычно используется для устройств, не поддерживающих прерывания, например, для определения поступления данных для чтения. |
xxhalt() | + | + | + | Вызывается для останова драйвера при останове системы или при выгрузке драйвера. |
xxstrategy() | - | + | + | Общая точка входа для операций блочного ввода/вывода. Название функции говорит о том, что устройство может обеспечивать собственную стратегию обработки поступающих запросов, например, изменять их порядок для повышения производительности ввода/вывода. Если устройство занято, функция помещает запросы в очередь. В этом случае фактический ввод/вывод инициирует функция обработки прерывания, которая вызывается, когда устройство закончит предыдущую операцию ввода/вывода. |
xxprint() | - | + | + | Выводит сообщение драйвера на консоль, обычно при запуске системы. |
Ядро вызывает те или иные функции драйвера в зависимости от запроса. Например, если процесс выполняет системный вызов read(2) для специального файла символьного устройства, ядро вызовет функцию xxread()
для соответствующего символьного драйвера. Если же процесс запрашивает ту же операцию для обычного дискового файла, ядро вызовет процедуру xxstrategy()
для блочного драйвера, обслуживающего данную файловую систему.
Вообще говоря, можно выделить пять основных случаев, в которых ядро обращается к функциям драйвера:
□ Автоконфигурация. Обычно происходит в процессе инициализации UNIX, когда ядро определяет, какие устройства доступны в системе.
□ Ввод/вывод. Запрос на операцию ввода/вывода может быть инициирован как прикладным процессом, так и некоторыми подсистемами ядра, например, подсистемой управления памятью.
□ Обработка прерываний. Ядро вызывает соответствующую функцию драйвера для обработки прерывания, поступившего от устройства (если устройство способно генерировать прерывания).
□ Специальные запросы. Ядро вызывает соответствующую функцию драйвера для обработки специальных команд, полученных с помощью системного вызова ioctl(2).
□ Реинициализация/Останов. Некоторые типы аппаратных архитектур могут требовать сброса и реинициализации устройства. Определенные функции драйвера также вызываются при останове операционной системы.
На рис. 5.2 и 5.3 приведены схемы доступа к драйверам символьного и блочного устройств.
Рис. 5.2. Доступ к драйверу символьного устройства
Рис. 5.3. Доступ к драйверу блочного устройства
Как видно из рисунков, схема обработки запроса ядром UNIX различна для символьных и блочных устройств.
При обсуждении точек входа драйверов устройств следует иметь в виду, что большинство функций драйвера, отвечающих за передачу данных, осуществляют копирование информации из адресного пространства ядра, в котором находится сам драйвер, в адресное пространство задачи. Когда ядро вызывает функцию драйвера, все действия выполняются в системном контексте процесса. Однако схема вызова функций может быть различной:
□ Функция может быть вызвана по запросу процесса. Например, если процесс выполняет системный вызов read(2), ядро вызывает соответствующую точку входа драйвера xxread()
, обеспечивающего работу с файлом. В этом случае говорят, что функция имеет контекст задачи.
□ Функция может быть вызвана другой подсистемой ядра операционной системы. Например, для блочного драйвера функция xxstrategy()
может быть вызвана страничным демоном, для сохранения страниц во вторичной памяти (как правило, на жестком диске). Поскольку страничный демон представляет собой системный процесс, выполняющийся только в контексте ядра, функция xxstrategy()
в этом случае имеет системный контекст.
□ Если функция вызывается в процессе обработки прерывания, то она имеет контекст прерывания – специальный вид системного контекста. Функции драйвера, отвечающие за обработку прерывания, например xxintr()
имеют этот тип контекста.
Различия в контексте и причинах вызова тех или иных функций драйвера позволяют представить драйвер устройства состоящим из двух частей: верхней части (top half) и нижней части (bottom half). Функции верхней части драйвера имеют синхронный характер, т.е. вызываются по определенным запросам прикладного процесса и выполняются в его контексте. Таким образом, для этих функций доступно адресное пространство и u-area процесса, и при необходимости эти функции могут перевести процесс в состояние сна (вызовом функции sleep()
ядра). Функции ввода/вывода и управления принадлежат верхней части драйвера.
Вызов функций нижней части носит асинхронный характер. Например, момент вызова функции обработки прерываний нельзя предугадать, и ядро не может контролировать, когда эта функция будет вызвана. Выполнение таких функций происходит в контексте ядра и обычно не имеет никакого отношения к контексту текущего процесса. Таким образом, функции системного контекста не имеют права адресовать структуры данных текущего процесса, например его u-area, а также не могут перевести процесс в состояние сна, поскольку это заблокирует процесс, не имеющий непосредственного отношения к работе драйвера.
Две части драйвера требуют синхронизации. Например, в случае, когда функции обеих частей используют одну и ту же структуру данных, функция верхней части при выполнении должна заблокировать прерывания на период работы с "разделяемой" областью памяти. В противном случае, прерывание может поступить в тот момент, когда целостность структуры данных нарушена, что приведет к непредсказуемым результатам.
Все представленные выше функции, за исключением xxhalt()
, xxpoll()
и xxintr()
, принадлежат верхней части драйвера. Функция xxhalt()
вызывается ядром при останове системы и, таким образом, имеет системный контекст, не связанный с контекстом прикладного процесса.
Функция xxpoll()
обычно вызывается при обработке ядром прерывания таймера для всех устройств, указанных как опрашиваемые. Это необходимо, в частности, для устройств, которые не могут или «не хотят» использовать аппаратные прерывания. Вместо этого xxpoll()
может использоваться для эмуляции прерываний, например вызывая функцию xxintr()
на каждый n-ный тик системного таймера. Поэтому и функция xxpoll()
и функция обработки прерывания xxintr()
не могут рассчитывать на контекст прикладного процесса. В большинстве версий UNIX функции опроса и обработки прерываний вызываются не через коммутатор устройств, а через специальные таблицы ядра.
В UNIX SVR4 определены две дополнительные точки входа – init()
и start()
. Драйвер регистрирует эти функции в таблицах ядра io_init[]
и io_start[]
. Код начальной загрузки системы запускает функции xxinit()
перед инициализацией ядра, а функции xxstart()
сразу же после инициализации.